创建类:

1
2
3
4
class Person {
public String name;
public int age;
}

创建实例:

1
Person ming = new Person();	  // Person ming是定义Person类型的变量,而new Person()是创建Person实例

在OOP中,classinstance是“模版”和“实例”的关系;定义class就是定义了一种数据类型,对应的instance是这种数据类型的实例;

class定义的field,在每个instance都会拥有各自的field,且互不干扰;访问实例字段的方法是变量名.字段名

通过new操作符创建新的instance,然后用变量指向它,即可通过变量来引用这个instance;指向instance的变量都是引用变量

方法

一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。

定义方法的语法是:

1
2
3
4
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值; // 如果没有返回值,返回类型设置为void,可以省略return
}

方法分为public方法,private方法。内部的方法可以调用private方法。

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

public void setBirth(int birth) {
this.birth = birth;
}

public int getAge() {
return calcAge(2019); // 调用private方法
}

// private方法:
private int calcAge(int currentYear) {
return currentYear - this.birth;
}
}

this 变量

在方法内部,可以使用一个隐含的变量this,它始终指向当前实例。因此,通过this.field就可以访问当前实例的字段。

如果没有命名冲突,可以省略this

1
2
3
4
5
6
7
class Person {
private String name;

public String getName() {
return name; // 相当于this.name
}
}

如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this

方法参数

调用方法时,必须严格按照参数的定义一一传递。

可变参数:可变参数相当于数组类型:

1
2
3
4
5
6
7
class Group {
private String[] names;

public void setNames(String... names) {
this.names = names;
}
}

调用时,可以这么写:

1
2
3
Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames(); // 传入0个String

如果把可变参数变成String[]类型...(String[] names) {},调用方需要自己先构造String[]

1
2
3
4
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]

//还可以传入null,可变参数可保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组
g.setNames(null);

参数绑定

基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
Person p = new Person();
int n = 15; // n的值为15
p.setAge(n); // 传入n的值
System.out.println(p.getAge()); // 15
n = 20; // n的值改为20
System.out.println(p.getAge()); // 还是15
}
}

class Person {
private int age;

public int getAge() {
return this.age;
}

public void setAge(int age) {
this.age = age;
}
}

引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
Person p = new Person();
String[] fullname = new String[] { "Homer", "Simpson" };
p.setName(fullname); // 传入fullname数组
System.out.println(p.getName()); // "Homer Simpson"
fullname[0] = "Bart"; // fullname数组的第一个元素修改为"Bart"
System.out.println(p.getName()); // Bart Simpson
}
}

class Person {
private String[] name;

public String getName() {
return this.name[0] + " " + this.name[1];
}

public void setName(String[] name) {
this.name = name;
}
}

注意下例:bob指向的内存改变

1
2
3
4
5
6
Person p = new Person();
String bob = "Bob";
p.setName(bob); // 传入bob变量
System.out.println(p.getName()); // "Bob"
bob = "Alice"; // bob改名为Alice
System.out.println(p.getName()); // "Bob"

构造方法

构造方法的名称就是类名。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。

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

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;
}
}

默认构造方法

如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,没有参数也没有执行语句。

如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来:

1
2
3
4
5
6
7
public Person() {
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

没有在构造方法中初始化字段时,引用类型的字段默认是null,数值类型的字段用默认值,int类型默认值是0,布尔类型默认值是false

创建对象实例的时候,按照如下顺序进行初始化:

  1. 先初始化字段,例如,int age = 10;表示字段初始化为10double salary;表示字段默认初始化为0String name;表示引用类型字段默认初始化为null
  2. 执行构造方法的代码进行初始化。

多构造方法

可以定义多个构造方法,在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分

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

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public Person(String name) {
this.name = name;
this.age = 12;
}

public Person() {
}
}

方法重载

在一个类中,如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。这种方法名相同,但各自的参数不同,称为方法重载(Overload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Hello {
public void hello() {
System.out.println("Hello, world!");
}

public void hello(String name) {
System.out.println("Hello, " + name + "!");
}

public void hello(String name, int age) {
if (age < 18) {
System.out.println("Hi, " + name + "!");
} else {
System.out.println("Hello, " + name + "!");
}
}
}

方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。

例:String类提供了多个重载方法indexOf(),可以查找子串:

  • int indexOf(int ch):根据字符的Unicode码查找;
  • int indexOf(String str):根据字符串查找;
  • int indexOf(int ch, int fromIndex):根据字符查找,但指定起始位置;
  • int indexOf(String str, int fromIndex)根据字符串查找,但指定起始位置。

JavaBean

如果读写方法符合以下这种命名规范:

1
2
3
4
5
6
7
8
9
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)

// 读方法:
public boolean isChild()
// 写方法:
public void setChild(boolean value)

那么这种class被称为JavaBean。我们通常把一组对应的读方法(getter)和写方法(setter)称为属性(property)。gettersetter也是一种数据封装的方法。

JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。

枚举JavaBean属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.beans.*;
public class Test_JavaBean {
public static void main (String[] args) throws Exception {
BeanInfo info = Introspector.getBeanInfo(Person1.class);
for(PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println(pd.getName());
System.out.println(" "+pd.getReadMethod());
System.out.println(" "+pd.getWriteMethod());
}
}
}

class Person1 {
private String name;
private int age;

public String getName() { // getter
return name;
}
public void setName(String name) { // setter
this.name = name;
}

public int getAge() {
return age;
}
public void steAge(int age) {
this.age = age;
}
}
/*
age
public int Person1.getAge()
public void Person1.setAge(int)
class
public final native java.lang.Class java.lang.Object.getClass()
null
name
public java.lang.String Person1.getName()
public void Person1.setName(java.lang.String)
*/

继承

子类从父类继承的时候,子类就获得了父类的所有功能,我们只需要为子类编写新增的功能。

注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!

Java使用extends关键字来实现继承 :

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) {...}
}

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

public int getScore() { … }

public void setScore(int score) { … }
}

在OOP的术语中,我们把Person称为超类(super class)/ 父类(parent class)/ 基类(base class),把Student称为子类(subclass)/ 扩展类(extended class)。

继承树

1
2
3
4
5
6
7
8
9
10
11
12
13
┌───────────┐
│ Object │
└───────────┘


┌───────────┐
│ Person │
└───────────┘


┌───────────┐
│ Student │
└───────────┘

没有明确写extends的类,编译器会自动加上extends Object。所以,任除了Object的类,都会继承自某个类。并且一个类有且仅有一个父类。

protected

子类无法访问父类的private字段或者private方法。而用protected修饰的字段可以被子类访问。一个protected字段和方法可以被其子类,以及子类的子类所访问 :

1
2
3
4
5
6
7
8
9
10
class Person {
protected String name;
protected int age;
}

class Student extends Person {
public String hello() {
return "Hello, " + name; // OK!
}
}

super

super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName

在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();如果父类的构造方法有参数,那么super()失效。子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。

1
2
3
4
5
6
7
8
class Student extends Person {
protected int score;

public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}

即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

向上转型

把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。向上转型实际上是把一个子类型安全地变为更加抽象的父类型:

1
2
3
4
5
6
Person p = new Student();
---
Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok

向下转型

如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)

1
2
3
4
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。

因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException

instanceof操作符,可以先判断一个实例究竟是不是某种类型:

1
2
3
4
5
6
7
Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false

Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true

从Java 14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:

1
2
3
4
5
6
7
8
9
10
11
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
// 可改写为-->
Object obj = "hello";
if (obj instanceof String s) {
// 可以直接使用变量s:
System.out.println(s.toUpperCase());
}

使用instanceof variable这种判断并转型为指定类型变量的语法时,必须打开编译器开关--source 14--enable-preview

继承 ? 组合

继承是is关系,组合是has关系。 具有has关系不应该使用继承,而是使用组合,即Student可以持有一个Book实例:

1
2
3
4
class Student extends Person {
protected Book book;
protected int score;
}

多态

在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)

Override和Overload不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override

加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。但是@Override不是必需的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
public void run() {
System.out.println("Person.run");
}
}
class Students extends Person {
// 不是Override,因为参数不同:
public void run(String s) { … }
// 不是Override,因为返回值不同:
public int run() { … }

@Override
public void run() {
System.out.println("Student.run");
}
}

多态

Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。即Person p = new Student()调用方法后是Student.run

多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class test_polymorphic {
public static void main(String[] args) {
Income[] incomes = new Income[] { new Income(3000), new Salary(7500), new StateCouncilSpecialAllowance(15000) };
System.out.println(totalTax(incomes));
}

public static double totalTax(Income... incomes) {
double total = 0;
for (int i = 0;i < incomes.length; i++) {
Income income = incomes[i];
total = total + income.getTax();
}
return total;
}
}

class Income {
protected double income;

public Income(double income) {
this.income = income;
}

public double getTax() {
return income * 0.1; // 税率10%
}
}

class Salary extends Income {
public Salary(double income) {
super(income);
}

@Override
public double getTax() {
if (income <= 5000) {
return 0;
}
return (income - 5000) * 0.2;
}
}

class StateCouncilSpecialAllowance extends Income {
public StateCouncilSpecialAllowance(double income) {
super(income);
}

@Override
public double getTax() {
return 0;
}
}

利用多态,totalTax()方法只需要和Income打交道,它完全不需要知道SalaryStateCouncilSpecialAllowance的存在,就可以正确计算出总的税。如果我们要新增一种稿费收入,只需要从Income派生,然后正确覆写getTax()方法就可以。把新的类型传入totalTax(),不需要修改任何代码。

覆写Object方法

Object定义了几个重要的方法:

  • toString():把instance输出为String
  • equals():判断两个instance是否逻辑相等;
  • hashCode():计算一个instance的哈希值。

在必要的情况下,我们可以覆写Object的这几个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {
...
// 显示更有意义的字符串:
@Override
public String toString() {
return "Person:name=" + name;
}

// 比较是否相等:
@Override
public boolean equals(Object o) {
// 当且仅当o为Person类型:
if (o instanceof Person) {
Person p = (Person) o;
// 并且name字段相同时,返回true:
return this.name.equals(p.name);
}
return false;
}

// 计算hash:
@Override
public int hashCode() {
return this.name.hashCode();
}
}

super调用

在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
protected String name;
public String hello() {
return "Hello, " + name;
}
}

Student extends Person {
@Override
public String hello() {
// 调用父类的hello()方法:
return super.hello() + "!";

final

继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
protected String name;
public final String hello() {
return "Hello, " + name;
}
}

Student extends Person {
// compile error: 不允许覆写
@Override
public String hello() {
}
}

如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承:

1
2
3
4
5
6
7
final class Person {
protected String name;
}

// compile error: 不允许继承自Person
Student extends Person {
}

类的实例字段,也可以用final修饰, 用final修饰的字段在初始化后不能被修改 。 对final字段重新赋值会报错:.

1
2
3
class Person {
public final String name = "Unamed";
}

可以在构造方法中初始化final字段,可以保证实例一旦创建,其final字段就不可修改。

抽象类

如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法 [ 类本身也需要声明为abstract ] :

1
2
3
abstract class Person {
public abstract void run();
}

使用abstract修饰的类就是抽象类。我们无法实例化一个抽象类。

面向抽象编程

当我们定义了抽象类Person,以及具体的Student等子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例:

1
Person s = new Student();

这样,我们可以对其方法进行调用而不关心Person变量的具体子类型。这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。

面向抽象编程的本质就是:

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

接口

抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。

如果一个抽象类没有字段,所有方法全部都是抽象方法 ,那么就可以把该抽象类改写成接口interface

1
2
3
4
interface Person {	// 接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来
void run();
String getName();
}

当一个具体的class去实现一个interface时,需要使用implements关键字。

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

public Student(String name) {
this.name = name;
}

@Override
public void run() {
System.out.println(this.name + " run");
}

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

一个类不能从多个类继承,但可以实现多个interface:

1
2
3
class Student implements Person, Hello { // 实现了两个interface
...
}

default方法

实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。

default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Person {
String getName();
default void run() {
System.out.println(getName() + " run");
}
}

class Student implements Person {
private String name;

public Student(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}
abstract class interface
继承 只能extends一个class 可以implements多个interface
字段 可以定义实例字段 不能定义实例字段
抽象方法 可以定义抽象方法 可以定义抽象方法
非抽象方法 可以定义非抽象方法 可以定义default方法

注意区分“接口”

Java的接口特指interface的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。

接口继承

一个interface可以继承自另一个interfaceinterface继承自interface使用extends,它相当于扩展了接口的方法。

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

interface Person extends Hello { // Person接口现在实际上有3个抽象方法签名
void run();
String getName();
}

继承关系

一般来说,公共逻辑适合放在abstract class中,具体逻辑放到各个子类,而接口层次代表抽象程度。可以参考Java的集合类定义的一组接口、抽象类以及具体子类的继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌───────────────┐
│ Iterable │
└───────────────┘
▲ ┌───────────────────┐
│ │ Object │
┌───────────────┐ └───────────────────┘
│ Collection │ ▲
└───────────────┘ │
▲ ▲ ┌───────────────────┐
│ └──────────│AbstractCollection │
┌───────────────┐ └───────────────────┘
│ List │ ▲
└───────────────┘ │
▲ ┌───────────────────┐
└──────────│ AbstractList │
└───────────────────┘
▲ ▲
│ │
│ │
┌────────────┐ ┌────────────┐
│ ArrayList │ │ LinkedList │
└────────────┘ └────────────┘

在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:

1
2
3
List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口