OOpre 作为作为具有北航计算机学院特色的 Pre 系列课程,以其不俗的码量和难以捉摸的 Junit 让每个 6 系学子爱之深而恨之切。我们在 OOPre 课程中的学习重点不在于 Java 复杂语法的掌握,而在于面向对象编程思想的初步建立,从而为我们之后的 OO 正课的学习打下基础。个人感觉对于 java 小白来说还是受益匪浅。
本文会先简单铺垫一点 java 基础语法,主要还是希望通过阐述面向对象编程常用的四大特性:封装 、继承 、多态和抽象 ,以及设计模式 ,递归下降法 等内容来帮助大家理解OOPre课程的核心内容。由于大家时间有限,本文不会大量堆砌 OOpre 课程代码,欲知2025 年秋季学期 OOPre 课程完整代码可以看笔者的个人GitHub仓库 。
同时本文也适合想要入门 Java 的同学,最后的 Java 项目编程规范 可以帮助大家写出更优雅的代码。
Java基础语法简介 static 读取 数据容器 final Character
面向对象编程课程内容简介 四大特性 封装 封装是面向对象编程的基本特性之一,相对易于理解,它将数据和操作数据的方法绑定在一起,隐藏了对象的内部实现细节。通过封装,我们可以保护对象的状态不被外部直接访问,从而提高代码的安全性和可维护性。在Java中,封装通常通过访问修饰符(如private、protected、public)来实现。一句话 :封装就是把属性私有化,通过公共方法来访问和修改属性。
看代码:
class Person { private String name = "JACK" ; public String getName () { return name; } }
继承 继承是面向对象编程的另一个重要特性,它允许我们创建一个新类(子类)来继承一个已有类(父类)的属性 和方法 。同时子类也可以重写 (Override)父类的方法或者定义子类特有的方法。通过继承,我们可以实现代码的重用和扩展,从而提高开发效率。在Java中,继承通过关键字extends来实现。一句话 :继承就是子类自动拥有父类的属性和方法。
看代码:
class Animal { String food ; public Animal (String food) { this .food = food; } public void eat () { System.out.println("I like eat " + food); } }class Dog extends Animal { private String gender; public Dog (String food,String gender) { super (food); this .gender = gender; } @Override public void eat () { System.out.println("Dog like eat " + food); } public void showGender () { System.out.println("My gender is " + gender); } }
在Main函数中使用时,编译器会优先调用子类的方法,如果子类没有重写父类方法会调用父类方法,但是如果父类也没有这个方法就会喜提Cannot resolve method。
多态 多态允许我们通过父类的引用来指向子类的对象,从而实现同一操作在不同对象上的不同表现形式。多态的实现方式主要有两种运行时多态和编译时多态。
运行时多态:通过方法重写实现,即子类重写父类的方法,在运行时根据对象的实际类型来决定调用哪个方法。
看代码:
class Person { public void speak () { System.out.println("Hello I am a person" ); } }class Student extends Person { @Override public void speak () { System.out.println("Hello I am a student" ); } }public class Main { public static void main (String[] args) { Person p1 = new Person (); Person p2 = new Student (); p1.speak(); p2.speak(); } }
编译时多态:通过方法重载实现,即在同一个类中定义多个同名但参数不同的方法,编译器根据传入参数的类型和数量来决定调用哪个方法。
看代码:
class MathUtils { public int add (int a, int b) { return a + b; } public double add (double a, double b) { return a + b; } }public class Main { public static void main (String[] args) { MathUtils mu = new MathUtils (); System.out.println(mu.add(2 , 3 )); System.out.println(mu.add(2.5 , 3.5 )); } }
抽象 抽象即隐藏具体的实现细节,只向外暴露清晰的使用接口。实现抽象主要有两种方法:抽象类 和接口 。
抽象类: 当我们发现一些类之间有共同的行为,但是这些行为在每个类中的具体实现方式不同,我们可以将这些共同的行为提取到一个抽象类中,并定义为抽象方法。
抽象类不能被实例化,只能被继承。 抽象类中可以包含抽象方法和具体方法。 抽象方法必须被子类重写。除非子类也是抽象类。
看代码:
abstract class Animal { public void breathe () { System.out.println("Animal is breathing" ); } public abstract void makeSound () ; } class Cat extends Animal { @Override public void makeSound () { System.out.println("Meow" ); } }
注意,抽象类只是不能被实例化,剩下的和普通类没什么区别,可以有构造方法、属性、具体方法等。
接口: 接口是一种更纯粹的抽象形式,他只包含方法的声明,没有任何实现。一句话:接口定义了一系列方法,这个方法必须被实现接口的类所实现。
接口不可以被实例化,只能被实现。 但是接口可以实例化他的实现类。
看代码:
interface Flyable { void fly () ; }class Bird implements Flyable { @Override public void fly () { System.out.println("Bird is flying" ); } }public class Main { public static void main (String[] args) { Flyable f = new Bird (); f.fly(); } }
注意,这里解释一下Flyable f = new Bird(),很多同学可能会有疑问,为什么不定义成Bird f = new Bird()呢?答案式为了实现代码解耦,提高扩展性。下面我们看一下两种实现方式的区别:
//方式一:接口引用指向实现类对象 如果我们未来想把 f换成一个Plane类的对象,只要Plane实现了Flyable接口,我们只需要直接将new Bird()改成new Plane()即可,而不需要修改其他代码。
//方式二:具体类引用 我们需要修改所有用到f的地方,把Bird全改成Plane,改着改着就错了😥。不信可以试试
设计模式 单例模式 单例模式确保一个类只能有一个实例,并提供一个全局访问点。简单来说就是利用唯一性来优化资源利用,保证数据一致 。这也就决定了单例模式的几个特性:
私有构造方法:防止外部通过new关键字创建多个实例。
私有静态变量:用于保存唯一的实例。
公有静态方法,提供一个全局访问点来获取实例。
单例模式的实现方式根据单例实例化时机 的不同分为饿汉式和懒汉式。
饿汉式: 在类加载时就创建实例,不管是否会用到,优点是线程安全,缺点是可能造成资源浪费。
public class Stone { private static final Stone stone = new Stone (); private Stone () {} public static Stone getInstance () { return stone; } }public class Main { public static void main (String[] args) { Stone s = Stone.getInstance(); } }
懒汉式: 在第一次调用获取实例的方法时才创建实例,优点是节省资源,缺点是需要处理线程安全问题。
public class Stone { private static Stone stone; private Stone () {} public static Stone getInstance () { if (stone == null ) { stone = new Stone (); } return stone; } }public class Main { public static void main (String[] args) { Stone s = Stone.getInstance(); } }
这里具体说明一下:
线程安全问题具体指的是如果多个线程同时调用getInstance()方法,可能会创建多个实例,违背了单例模式的初衷。解决方法有很多,比如使用synchronized关键字来锁定方法:public static synchronized Stone getInstance() {...}。大家只做了解即可,现阶段还不会用到。
工厂模式 工厂模式通过定义一个接口或抽象类来创建对象,而不是直接实例化具体的类。这样可以将对象的创建逻辑与使用逻辑分离,提高代码的灵活性和可维护性。工厂模式主要有三种类型:简单工厂模式、工厂方法模式和抽象工厂模式。核心价值在于解耦和扩展性。
我们先来看一个没有工厂的情景:假设你需要创建不同类型的手机对象,直接在代码中new实例:
Phone Huawei = new HuaweiPhone ();Phone Apple = new ApplePhone ();
问题很明显:
耦合严重 :使用者必须知道具体的实现类(HuaweiPhone、Iphone),如果后续替换手机类型(如换成小米),需要修改所有new的地方;
扩展性差 :新增手机类型时,需要修改所有使用方的代码,违反 “开放 / 封闭原则”。
工厂模式的作用就是解决这些问题,把对象创建的”复杂活”交给工厂,使用者只需要”提需求”。
简单工厂模式(静态工厂模式): 通过一个工厂类根据传入的参数决定创建哪种具体产品类的实例。
public class PhoneFactory { public static Phone createPhone (String type) { if (type.equals("huawei" )) { return new HuaweiPhone (); } else if (type.equals("..." )) { return new ...(); } } }
工厂方法模式:为了解决简单工厂的”职责过重”问题,工厂方法模式将”创建不同产品”的逻辑,拆分到多个具体工厂类中,每个工厂只负责创建一种产品。
interface PhoneFactory { Phone createPhone () ; }public class HuaweiFactory implements PhoneFactory (){ @Override public Phone createPhone () { return new HuaweiPhone (); } }PhoneFactory huaweiFactory = new HuaweiFactory ();Phone huawei = huaweiFactory.createPhone();
抽象工厂模式: 抽象工厂模式进一步将工厂方法模式进行抽象,提供一个接口用于创建一系列相关或相互依赖的对象,而无需指定它们具体的类。一个具体工厂负责创建一组相关产品。
interface ElectronicFactory { Phone createPhone () ; Earphone createEarphone () ; }public class HuaweiFactory implements ElectronicFactory { @Override public Phone createPhone () { return new HuaweiPhone (); } @Override public Earphone createEarphone () { return new HuaweiEarphone (); } }
观察者模式 观察者模式定义了一种一对多 的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。观察者模式主要包含两个角色:主题(Subject)和观察者(Observer)。
主题对象只需要知道它有哪些观察者,而不需要知道每个观察者的具体实现细节。同样,观察者也只需要关注主题对象的状态变化,而不需要知道主题对象的内部逻辑。这就极大程度地实现了代码的解耦。
为了清楚的阐释观察者模式的实现方式,我们以气象站为例,温度,湿度,气压显示器为观察者。
interface Subject { void registerObserver (Observer o) ; void removeObserver (Observer o) ; void notifyObservers () ; }interface Observer { void updata (float temperature,float humidity,float pressure) ; }public class WeatherData implements Subject { private List<Observer> observers; private float temperature; private float humidity; private float pressure; public WeatherData () { observers = new ArrayList <>(); } @Override public void registerObserver (Observer o) { observers.add(o); } @Override public void removeObserver (Observer o) { observers.remove(o); } @Override public void notifyObservers () { for (Observer o : observers) { o.update(temperature, humidity, pressure); } } }public class Table implements Observer { @Override public void updata (float temperature,float humidity,float pressure) { } }
递归下降法简介 递归下降法是一种自顶向下的语法分析 方法,通常用于编译器和解释器中。它通过递归地调用函数来处理输入的符号序列,从而构建语法树。递归下降法的核心思想是将每个非终结符对应一个函数,这些函数根据文法规则来解析输入。一句话 :递归下降法就是用函数调用模拟语法规则的推导过程。
这个说法有点抽象,大家可能云里雾里,我们看一个简单的例子:
这里我们以雇佣关系为例,A(B(E(G),F),C,D)表示,A雇佣B,C,D,B雇佣E,F,E雇佣G。
形式化表述 :
变量说明 :
Token:词法单元,是指从源代码中分解出来的、具有独立语法意义的最小单元。 Lexer:词法分析器,核心功能是读取字符,输出 Token。
看代码:
public class Lexer { private final String input; private int pos = 0 ; private String curToken; private String removeWhiteSpace (String s) { return s.replaceAll("\\s+" ,"" ); } private boolean isIdChar (char c) { return Character.isLetter(c) || Character.isDigit(c) || c == '_' ; } private String getId () { StringBuilder sb = new StringBuilder (); while (pos < input.length() && isIdChar(input.charAt(pos))) { sb.append(input.charAt(pos)); pos++; } return sb.toString(); } public void next () { if (pos == input.length()) { curToken = null ; return ; } char c = input.charAt(pos); if (isIdChar(c)) { curToken = getId(); } else if (c == '(' || c == ')' || c == ',' ){ pos += 1 ; curToken = String.valueOf(c); } else { throw new RuntimeException ("Unexpected Character" + c); } } public Lexer (String input) { this .input = removeWhiteSpace(input); next(); } public String peek () { return curToken; } public void match (String target) { if (target.equals(curToken)) { next(); } else { throw new RuntimeException ("Excepted token :" + target + ", but got: " + curToken); } } }
以上是Lexer的完整代码,主要功能就是依次拆解出Token,然后应用到Main函数。注意一下match和peek的区别,match自带next方法,peek只监测curToken。同时pos永远指向即将解析的Token。
在Main中调用时需要写一个递归函数 parseEmployees来调用解析器。
private static void parseEmployees (Lexer lexer,String employer) { while (true ) { String token = lexer.peek(); if (token == null || token.equals(')' )) { break ; } String employee = token; lexer.next(); if ('(' .equals(lexer.peek())) { lexer.match('(' ); parseEmployees(lexer,employee); lexer.match(')' ); } else if (',' .equals(lexer.peek())) { lexer.match(',' ); } } }
递归函数退出的时候条件是),因此我们需要用match方法消耗掉这个token,从而继续向后执行。
Main: Lexer lexer = new Lexer (input); String topEmployer = lexer.peek(); lexer.next(); lexer.match("(" ); parseEmployees(lexer, topEmployer); lexer.match(")" );
面向对象编程总结 以上就是OOPre课程的核心内容简介,如果你看到这里,那么恭喜你应该可以速通OOPre,面向对象编程的思想方法是我们以后学习编程的基础,希望大家能理解并灵活运用到实际编程中。接下来我们将进入Java项目编程规范的介绍,这里我们采用阿里巴巴Java开发手册作为参考标准。
Java项目编程规范简介 命名风格
类名使用大驼峰 命名法,例:XmlService。
方法名,参数名,变量名使用小驼峰 命名法,例:getUserName。
常量名使用全大写字母,单词之间用下划线分隔,例:MAX_SIZE。
中括号是数组定义的一部分,不能与类型分开,例:int[] arr。
在POJO类中,布尔类型变量不要使用is开头,例:boolean active; 而不是 Boolean isActive;。
接口类中的方法属性不要任何修饰变量,public也不要加。
代码格式
大括号为空写成{},不换行。大括号非空时写法参考代码示例。
左/右小括号和字符之间不要有空格。
if/for/while/switch等关键字与左小括号之间要有一个空格。
任何二目,三目运算符前后都要有空格。
public static void main (String[] args) { String str = "Hello World" ; int flag = 0 ; if (flag == 0 ) { System.out.println(str); } else { System.out.println("Flag is not zero" ); } }
面向对象变成规范
类内方法定义的顺序是:公有方法或保护方法 > 私有方法 > getter/setter方法。
getter/setter方法内不增加业务逻辑,只return this.data。
Lyrics Shring 我只想要拉住流年 好好的说声再见 遗憾感谢都回不去昨天 我只想铭记这瞬间 我们一起走过的光年 6月后 光年成纪念
未完待续 虽然 OOpre 的征程告一段落了,但是这篇写于 2025 年 1 月的文章还没有结束,笔者会在后续的学习中完善 Java 项目编程规范的内容,欢迎大家持续关注