面向对象

面向对象的三个基本特征是:封装、继承、多态

Class & Instance


  • class是对象模板,instance是对象实例
  • 定义class(包含多个字段,field
1
2
3
4
class Person {
public String name;
public int age;
}
  • 创建instance(指向instance的变量都是引用变量)
1
Person ming = new Person();
  • 访问实例变量用变量.字段
  • 一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中

方法


  • 动机:为了避免外部代码直接去访问field,我们可以用private修饰field,拒绝外部访问,然后使用method来让外部代码可以间接修改field
    • 在方法内部,我们有机会检查参数对不对;而外部代码没有任何机会把字段设置成不合理的值
    • 一个类通过定义方法,可以给外部代码暴露一些操作的接口,同时保证内部逻辑一致性
    • 调用方法的语法是实例变量.方法名(参数);
  • 定义方法
1
2
3
4
5
6
7
8
9
// 修饰符 方法返回类型 方法名(方法参数列表) {
// 若干方法语句;
// return 方法返回值;
// }
// 如果没有返回值,返回类型设置为void,可以省略return

public String getName() {
return this.name;
}
  • private方法:不允许外部调用,只有类的内部可以调用
  • this变量:在方法内部,可以使用一个隐含的变量this,它始终指向当前实例。通过this.field就可以访问当前实例的字段
    • 如果没有命名冲突,可以省略this;如果局部变量和字段重名,就必须加上this
  • 方法参数:0或任意个,用于接收传递给方法的变量值
  • 可变参数:类型…,相当于数组类型
    • 可变参数可以保证无法传入null
  • 参数绑定
    • 基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响
    • 引用类型参数的传递,调用方的变量和接收方的参数变量指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象)

构造方法


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Main {
public static void main(String[] args) {
Person p = new Person("Xiao Ming", 15);
}
}

class Person {
private String name;
private int age;

// 构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return this.name;
}

public int getAge() {
return this.age;
}
}
  • 实例在创建时通过new操作符会调用其对应的构造方法,构造方法用于初始化实例
    • 创建实例实际上是通过构造方法来初始化实例的
    • 构造方法的名称就是类名
    • 构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new
    • 没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法
    • 没有在构造方法中初始化字段时,引用类型的字段默认是null,数值类型的字段用默认值,int类型默认值是0
      ,布尔类型默认值是false

多构造方法

  • 可以定义多个构造方法,编译器根据参数自动判断
  • 可以在一个构造方法内部调用另一个构造方法,便于代码复用

重载


1
2
3
4
5
6
7
8
9
10
11
12
// String类提供了多个重载方法indexof()
public class Main {
public static void main(String[] args) {
String s = "Test string";
int n1 = s.indexOf('t');
int n2 = s.indexOf("st");
int n3 = s.indexOf("st", 4);
System.out.println(n1);
System.out.println(n2);
System.out.println(n3);
}
}
  • 含义:方法重载是指多个方法的方法名相同,但各自的参数不同
  • 作用:功能类似的方法使用同一名字,更容易记住,调用起来更简单
  • 方法重载的返回值类型通常是相同的

继承


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
private String name;
private int age;

public String getName() {...}
public void setName(String name) {...}
public int getAge() {...}
public void setAge(int age) {...}
}

class Student extends Person {
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;

public int getScore() { … }
public void setScore(int score) { … }
}
  • 作用:复用代码
  • 使用extends关键字来实现继承
  • 继承树:没有明确写extends的类,编译器会自动加上extends Object
    • 任何类,除了Object,都会继承自某个类
    • 一个类有且仅有一个父类。只有Object特殊,它没有父类
  • 子类无法访问父类的private,可以访问父类的protected
  • 如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法
    • 子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的

阻止继承

1
2
3
4
public sealed class Shape permits Rect, Circle, Triangle {
...
}
// 上述Shape类只允许指定的3个类(Rect, Circle, Triangle)继承它
  • 从Java15开始,使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称
    • sealed目前是预览状态,要启用它,必须使用参数--enable-preview--source 15

向上转型 Upcasting

1
Person p = new Student();
  • 把一个子类型安全地变为更加抽象的父类型
    • 例如,继承树是Student > Person > Object,那么可以把Student类型转型为Person,或者更高层次的Object

向下转型 Downcasting

1
2
3
4
5
Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}
  • 很可能会失败。为了避免出错,使用instanceof先判断一个实例究竟是不是某种类型
  • 从Java 14开始,判断instanceof后,可以直接转型为指定变量,直接使用,避免再次强制转型

继承 & 组合

  • 继承是is关系,组合是has关系

多态


1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
public void run() { … }
}

class Student extends Person {
@Override
public void run() { … }
}

class Teacher extends Person {
@Override
public void run() { … }
}
  • 含义:针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法
  • 作用:允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码

覆写 Override

  • 子类可以覆写父类的方法,覆写在子类中改变了父类方法的行为
    • 加上@Override可以让编译器帮助检查是否进行了正确的覆写
  • 在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用
  • final
    • final修饰的方法可以阻止被覆写
    • final修饰的class可以阻止被继承
    • final修饰的field必须在创建对象时初始化,随后不可修改

抽象类


1
2
3
abstract class Person {
public abstract void run();
}
  • 动机:如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么可以把父类的方法声明为抽象方法,本身没有实现任何方法语句
  • 含义:如果一个class定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract修饰;因为无法执行抽象方法,所以这个类也必须申明为抽象类(abstract class)
    • 抽象类无法实例化
  • 作用:抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。相当于定义了“规范”

面向抽象编程

  • 上层代码只定义规范
  • 不需要子类就可以实现业务逻辑(正常编译)
  • 具体的业务逻辑由不同的子类实现,调用者并不关心

接口


1
2
3
4
interface Person {
void run();
String getName();
}
  • 动机:如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口
    • 接口是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有
  • 使用interface声明一个接口
  • 当一个具体的class去实现一个interface时,需要使用implements关键字;一个类可以实现多个interface
  • 接口也是数据类型,适用于向上转型和向下转型

接口继承

1
2
3
4
5
6
7
8
interface Hello {
void hello();
}

interface Person extends Hello {
void run();
String getName();
}
  • 使用extends
  • 继承关系
    • 公共逻辑适合放在abstract class中,具体逻辑放到各个子类,而接口层次代表抽象程度
    • 实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象

default方法

  • 接口可以定义default方法
  • 动机:当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法
  • default方法和抽象类的普通方法的区别:因为interface没有字段,所以default方法无法访问字段,而抽象类的普通方法可以访问实例字段

静态字段 & 静态方法


静态字段

  • static修饰的字段称为静态字段
  • 实例字段&静态字段
    • 实例字段在每个实例中都有自己的一个独立“空间”,各个实例的同名字段互不影响
    • 静态字段只有一个共享“空间”,所有实例都会共享该字段
  • 不推荐用实例变量.静态字段去访问静态字段,推荐用类名来访问静态字段
    • 可以把静态字段理解为描述class本身的字段(非实例字段)

静态方法

  • static修饰的方法称为静态方法
  • 实例方法&静态方法
    • 调用实例方法必须通过一个实例变量
    • 调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数
  • 调用静态方法不需要实例,无法访问this,但可以访问静态字段和其他静态方法
  • 静态方法常用于工具类(如Arrays.sort()Math.random() )和辅助方法(如Java程序的入口main() 就是静态方法)

接口的静态字段

  • interface可以有静态字段,并且静态字段必须为final类型
  • 因为interface的字段只能是public static final类型,所以可以去掉这些修饰符,简写


1
2
3
4
package ming; // 申明包名ming

public class Person {
}
  • 在定义class的时候,需要在第一行声明这个class属于哪个包
  • 一个类总是属于某个包(package),类名(比如Person)只是一个简写,真正的完整类名是包名.类名
    • 只要包名不同,类就不同
    • 包可以是多层结构,用.隔开
    • 包没有继承关系
    • 没有定义包名的class使用的是默认包,非常容易引起名字冲突,不推荐不写包名的做法

包作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package hello;

public class Person {
// 包作用域:
void hello() {
System.out.println("Hello!");
}
}

public class Main {
public static void main(String[] args) {
Person p = new Person();
p.hello(); // 可以调用,因为Main和Person在同一个包
}
}
  • 位于同一个包的类,可以访问包作用域的字段和方法
  • 不用publicprotectedprivate修饰的字段和方法就是包作用域

import

1
2
3
4
5
6
7
8
9
10
11
// Person.java
package ming;

// 导入完整类名:
import mr.jun.Arrays;

public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}
  • JDK的核心类使用java.lang包,编译器会自动导入
  • JDK的其它常用类定义在java.util.*java.math.*java.text.*,……

作用域


public

  • 定义为publicclassinterface可以被其他任何类访问
  • 定义为publicfieldmethod可以被其他类访问,前提是首先有访问class的权限
  • 如果不确定是否需要public,就不声明为public,即尽可能少地暴露对外的字段和方法
  • 一个.java文件只能包含一个public类,但可以包含多个非public类。如果有public类,文件名必须和public类的名字相同

private

  • 定义为privatefieldmethod无法被其他类访问
  • private访问权限被限定在class的内部,而且与方法声明顺序无关。推荐把private方法放到后面
  • 嵌套类(nested class)拥有访问private的权限

protected

  • 作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类

pakage

  • 包作用域是指一个类允许访问同一个package的没有publicprivate修饰的class,以及没有publicprotectedprivate修饰的字段和方法
  • 把方法定义为package权限有助于测试

局部变量

  • 在方法内部定义的变量称为局部变量
  • 局部变量作用域从变量声明处开始到对应的块结束
  • 方法参数也是局部变量
  • 应该尽可能把局部变量的作用域缩小,尽可能延后声明局部变量

final

  • 可以阻止被继承、阻止被子类覆写、阻止被重新赋值

内部类 Nested Class


Inner Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested"); // 实例化一个Outer
Outer.Inner inner = outer.new Inner(); // 实例化一个Inner
inner.hello();
}
}

class Outer {
...
class Inner {
// 定义了一个Inner Class
...
}
}
  • Inner Class的实例不能单独存在,必须依附于一个Outer Class的实例
    • 要实例化一个Inner,必须首先创建一个Outer的实例,然后,调用Outer实例的new来创建Inner实例
  • Inner Class可以修改Outer Class的private字段
  • Outer类被编译为Outer.class,而Inner类被编译为Outer$Inner.class

Anonymous Class

1
2
3
Runnable r = new Runnable() {
// 实现必要的抽象方法...
};
  • 另一种定义Inner Class的方法。它不需要在Outer Class中明确地定义这个Class
    • 动机:因为在这里我们不关心类名,比直接定义Inner Class可以少写很多代码
  • 匿名类也可以访问Outer Class的private字段和方法
  • 匿名类被编译为Outer$1.classOuter$2.classOuter$3.class
  • 除了接口外,匿名类也可以继承自普通类
  • Inner Class和Anonymous Class本质上是相同的,都必须依附于Outer Class的实例,即隐含地持有Outer.this实例,并拥有Outer Class的private访问权限

Static Nested Class

  • 不再依附于Outer的实例,而是一个完全独立的类,因此无法引用Outer.this,但可以访问Outerprivate静态字段和静态方法
  • Static Nested Class是独立类,但拥有Outer Class的private访问权限

classpath和jar


  • JVM通过环境变量classpath决定搜索class的路径和顺序
  • 不推荐设置系统环境变量classpath,始终建议通过-cp命令传入
  • jar包相当于目录,可以包含很多.class文件,方便下载和使用

class版本


  • 高版本的JDK可编译输出低版本兼容的class文件,反之可能报错
  • 运行时使用哪个JDK版本,编译时就尽量使用同一版本编译源码

模块


  • .class文件是JVM看到的最小可执行文件,而一个大型程序需要编写很多Class,并生成一堆.class文件。jar文件就是class文件的容器,为了方便管理。它并不关心class之间的依赖
  • 从Java 9开始引入了模块(Module),自带“依赖关系”的class容器就是模块,模块以.jmod扩展名标识
  • 只有java.base模块不依赖任何模块,是“根模块”
  • 把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且可以包含二进制代码(通常是JNI扩展)、支持多版本
  • 模块的作用:声明并管理依赖关系;进一步隔离代码的访问权限

附录


教程

廖雪峰的Java教程-面向对象基础

2022年12月 Java 入门

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×