Java进阶01-面向对象高级

本文介绍面向对象的进阶内容,包括static关键字、继承、多态、抽象类、接口、内部类、枚举和泛型等

1. static

可以修饰成员变量、成员方法。

1.1 类变量

成员变量按有无static修饰分为两类:

  • 类变量:有static修饰,属于类,在计算机中只有一份,会被类和类的所有对象共享,可以通过类名直接访问。
  • 实例变量:无static修饰,属于对象,必须先创建实例,然后通过实例访问。

应用场景:当数据只需要一份,且希望被共享使用或修改,可以使用类变量。

1.2 类方法

成员方法按有无static分为两类:

  • 类方法:有static修饰的方法,属于类,可以用类名直接调用。
  • 实例方法:无static修饰的方法,属于对象,只能通过实例访问。

应用场景:设计工具类,可以通过类名直接调用,调用方便,提升了代码复用性和效率,节省内存。由于工具类不需要创建对象,建议将工具类的构造方法私有化。

注意事项:

  1. 类方法中可以访问类成员,不能直接访问实例成员。
  2. 实例方法中可以直接访问类成员,也可以直接访问实例成员。
  3. 实例方法中可以出现this关键字,类方法中不能出现this关键字。

1.3 应用

1.3.1 代码块

代码块是类的五大成分之一(成员变量、构造器、方法、代码块、内部类)。

代码块分为两种:

  • 静态代码块
    • 格式:static {}
    • 特点:类加载时自动执行,由于类只会加载一次,所以静态代码块也只会执行一次。
    • 作用:完成类的初始化,例如:对类变量的初始化。
  • 实例代码块
    • 格式:{}
    • 特点:每次创建对象时,执行实例代码块,并在构造器前执行。
    • 作用:和构造器一样,都是用来完成对象的初始化,例如:对实例变量进行初始化赋值。

1.3.2 单例设计模式

单例设计模型

  • 解决什么问题:确保一个类只有一个实例,可以避免浪费内存。
  • 饿汉式怎么写
    • 把类的构造器私有化。
    • 定义一个类变量,记住类的一个对象。
    • 定义一个类方法,返回对象。
1
2
3
4
5
6
7
8
9
10
// 饿汉式:迫不及待,确保想要就已经有了
public class Singleton {
private static Singleton singleton = new Singleton();

private Singleton() {}

public static Singleton getSingleton() {
return singleton;
}
}
  • 懒汉式怎么写
    • 把类的构造器私有化。
    • 定义一个类变量用于存储对象。
    • 提供一个类方法,保证返回的时同一个对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 懒汉式:要用到的时候才创建
public class Singleton2 {
private static Singleton2 singleton2;

private Singleton2(){}

private static Singleton2 getSingleton2() {
if (null == singleton2) {
singleton2 = new Singleton2();
}
return singleton2;
}
}

建议:如果单例需要频繁使用,可以用饿汉式;如果单例使用不频繁,可以用懒汉式。

2. 继承

2.1 认识继承

Java中提供了一个关键字extends,用这个关键字,可以让一个类和另一个类建立起父子关系。

1
2
3
public class B extends A {

}
  • 继承的特点:子类能继承父类的非私有方法(成员变量、成员方法),可以直接使用。
  • 继承后对象的创建:子类的对象是由子类、父类共同完成的。

2.2 继承的使用

好处:减少重复代码的编写,提高代码的复用性。

注意事项:

  1. 权限修饰符
  2. 单继承、Object类
  3. 方法重写
  4. 子类访问其他成员的特点
  5. 子类构造器的特点

权限修饰符

  • private:只能在本类中访问
  • default(不写):本类和同一个包的类可以访问
  • protected:本类、同一个包的类、任意包的子类可以访问
  • public:任何类都能访问

注意:protected修饰的成员,可以在子类内部访问,而非子类对象可以对父类的成员进行调用。即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Parent {
protected int number;
}
// protected正确用法
public class Child {
void func() {
super.number = 1;
}
}
// protected错误用法
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.number = 1;
}
}

单继承

Java不支持多继承,即一个子类只能直接继承一个父类;

但Java支持多层继承,即父类还可以继承父类,类似于祖、父、子三代单传。

为什么不支持多继承?

若支持多继承,两个父类中出现同名方法,子类不知道继承哪个方法。

Object类

Object类是Java中所有子类的祖宗类,任何类的继承关系向上追溯,源头都是Object类。

自己创建的类,Java默认会使其继承Object类。

任何类都可以直接调用Object的方法。

方法重写

当子类认为父类的方法不好用,或者无法满足自己的需求时,可以重写一个方法名称、参数列表一样的方法,来代替父类的这个方法,这就是方法重写。

注意事项:

  1. 使用@Override注解,它可以检查方法重写的格式是否正确,代码可读性也更好。
  2. 子类重写父类方法时,访问权限必须大于等于父类方法的权限。
  3. 重写方法的返回值类型必须和被重写方法一样,或者范围更小。
  4. 私有方法、静态方法不能被重写。

“声明不变,重新实现”

子类访问其他成员的特点

在子类中访问其他成员(成员变量、成员方法),是依照“就近原则”的,即变量名相同的情况下,访问优先级:局部变量 > 子类成员变量 > 父类成员变量。

可以在访问时添加关键字,来声明要访问的成员。

  • 访问父类的成员:super
  • 访问子类的成员:this
  • 访问局部变量:不加

子类构造器的特点

子类的全部构造器,都会先调用父类的构造器,再执行自己。

子类构造器默认存在super()方法,因此默认会调用父类的无参构造器。

若父类中只存在有参构造器,子类构造器会报错,需要显式地声明一个父类无参构造器,或者在子类构造器中显式地调用父类有参构造器。

应用场景:子类的成员变量拆分到父类和子类中,为了能够完整地构造成员变量,需要调用父类的构造器。

注意事项:

  • 调用本类的构造器:this(构造参数);调用父类的构造器:super(构造参数)。
  • this(构造参数)、super(构造参数)都只能写在构造器的第一行,因此二者不能同时出现。

3. 多态

3.1 认识多态

多态是在继承/实现情况下的一种现象,表现为:对象多态、行为多态。

多态的前提:

  • 有继承/实现关系。
  • 存在父类引用子类对象,如People p1 = new Teacher();
  • 存在方法重写。

注意:多态是对象、行为的多态,Java中的属性(成员变量)是不谈多态的,引用是什么类就使用什么类的成员变量。

3.2 多态的作用

多态的好处

  • 在多态形式下,右边的对象是耦合的,更便于扩展和维护。
  • 定义方法时,使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。
  • 多态下不能使用子类独有的方法,需要进行类型转换。

类型转换

  • 自动类型转换:父类 变量名 = new 子类();
  • 强制类型转换:子类 变量名= (子类) 父类变量;

注意事项

  • 存在继承/实现关系可以在编译阶段进行强制类型转换,编译阶段不会报错。
  • 运行时,如果发现对象的真实类型与强转后的类型不同,会报类型转换异常(ClassCastExecption)。
  • 强转前,建议使用instanceof关键字,判断当前对象的真实类型,再进行强转:o instanceof 类型

4. 关键字、抽象类

4.1 final关键字

final关键字

final关键字是最终的意思,可以修饰(类、方法、变量)

  • 修饰类:该类被称为最终类,特点是不能被继承了。
  • 修饰方法:该方法被称为最终方法,特点是不能被重写了。
  • 修饰变量:该变量只能被赋值一次。

final修饰变量的注意事项

  • final修饰基本类型的变量,变量存储的数据不能被改变。
  • final修饰引用类型的变量,变量存储的地址不能被改变,但地址所指向对象的内容是可以被改变的。

常量

什么是常量

  • 常量是使用了static final 修饰的成员变量,通常用于记录系统的配置信息。
  • 命名规范:建议使用大写英文单词,多个单词之间用下划线相连。

使用常量记录系统配置信息的优势、执行原理

  • 代码可读性更好,可维护性也更好。
  • 程序编译后,常量会被“宏替换”:出现常量的地方全部会被替换成其记住的字面量,这样可以保证使用常量和直接用字面量的性能是一样的。

4.2 抽象类

什么是抽象类

多个类中只要有重复代码(包括相同的方法签名),我们都应该抽取到父类中去,此时,父类中就有可能存在只有方法签名的方法,这时,父类必定是一个抽象类了,我们抽出这样的抽象类,就是为了更好地支持多态。

简言之,只有方法签名(方法名、参数、返回值)的方法称为抽象方法;而包含抽象方法的类称为抽象类。抽象类中可以现有方法体的正常的成员方法。

抽象方法与抽象类需要用abstract关键字修饰,如下就是一个抽象类A:

1
2
3
4
5
6
7
8
public class abstract A {
// 抽象方法
abstract void func1();
// 正常方法
void func2() {
System.out.println("我是正常方法");
}
}

抽象类的应用常用和好处是什么

抽象类有两种主要的应用场景:

  • 支持多态:使用抽象类,可以将子类中相同的代码(包括方法签名)都抽取出来,可以更好地支持多态,以提高代码的灵活性。
  • 提升扩展性:在不知道业务系统未来具体的实现时,可以先定义抽象类,将来让子类去继承实现,以方便系统的扩展。

4.3 模板方法设计模式

模板方法设计模式是抽象类的经典应用。

模板方法设计模式主要解决方法中存在重复代码的问题。如果不同类的方法中,存在大部分相同的代码,仅有部分不相同,则可以将相同代码抽取出来作为模板方法,不同代码则作为抽象方法,交给子类各自实现,则有利于降低代码重复,提高复用率和可扩展性。

模板方法的书写过程:

  • 定义一个抽象类。
  • 在抽象类中定义两个方法
    • 模板方法:存放相同部分的代码。
    • 抽象方法:具体实现交给子类。

模板方法改造如下例:

改造前:

1
2
3
4
5
6
7
public class A {
public void sing() {
System.out.println("我想唱一首歌");
System.out.println("要做神仙 驾鹤飞天");
System.out.println("我唱完了");
}
}
1
2
3
4
5
6
7
public class B {
public void sing() {
System.out.println("我想唱一首歌");
System.out.println("点石成金 妙不可言");
System.out.println("我唱完了");
}
}

这里两个类的方法有相同的部分,也有不同的部分,可以适用模板方法进行改造。

改造后:

1
2
3
4
5
6
7
8
9
10
public class C {
// 将相同部分代码抽取为模板方法
public abstract void sing() {
System.out.println("我想唱一首歌");
singASong();
System.out.println("我唱完了");
}
// 不同部分代码作为抽象方法,交由子类去具体实现
public abstract void singASong();
}
1
2
3
4
5
6
7
8
// 继承C类
public class A extends C{
// 重写抽象方法
@Override
public void singASong() {
System.out.println("要做神仙 驾鹤飞天");
}
}
1
2
3
4
5
6
7
8
// 继承C类
public class B extends C{
// 重写抽象方法
@Override
public void singASong() {
System.out.println("点石成金 妙不可言");
}
}

最后调用A、B的sing()方法如下:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
C a = new A();
a.sing();
C b = new B();
b.sing();
}
}

输出结果:

1
2
3
4
5
6
我想唱一首歌
要做神仙 驾鹤飞天
我唱完了
我想唱一首歌
点石成金 妙不可言
我唱完了

5. 接口

5.1 认识接口

Java提供了一个关键字interface,用这个关键字可以定义出一个特殊的结构:接口。

1
2
3
4
public interface 接口名 {
// 成员变量(常量)
// 成员方法(抽象方法)
}

需要注意的是,成员变量默认使用public static final修饰,成员方法默认使用public abstract修饰。

注意事项:

  • 接口不能创建对象
  • 接口是用来被类实现(implements)的,实现类的接口称为实现类。
1
2
3
修饰符 class 实现类 implements 接口1,接口2,接口3... {

}
  • 一个类可以实现多个接口,实现类实现多个接口,必须重写全部接口的全部抽象方法,否则实现类需要定义为抽象类。

5.2 接口的作用

接口的作用

  • 可以解决单类继承的问题,通过接口,可以让一个类在已经继承的情况下,还可以通过实现接口来扩展自己的功能。
  • 通过实现接口,可以显式地表明类所包含的功能,可以放心地调用。
  • 面向接口编程,一个接口可以被多个类实现,其方法可以包括不同的实现,可以方便灵活地切换各种业务。

JDK 8开始新增的接口方法

JDK 8开始,接口新增了几种方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface A {
// 1. 默认方法(实例方法)
// 使用default修饰,默认加上public,只能使用接口的实现类对象调用
default void func1() {
...
}
// 2. 私有方法
// 必须使用private修饰,JDK 9开始支持
private void func2() {
...
}
// 3. 静态方法(类方法)
// 使用static修饰,默认加上public,只能使用类名调用
static void func3(){
...
}
}

从JDK 8开始,接口中新增这些方法,可以增强接口的能力,更便于项目的扩展和维护。

例如默认方法,如果一个接口被多个类实现,在后期开发中需要在接口中新增一个通用方法,此时再去实现类中一一实现接口方法,显然是很繁琐的。因此我们可以直接在接口中定义一个默认方法(default),这样所有该接口的实现类都可以直接调用这个方法。

接口的注意事项(了解)

  • 一个接口可以继承多个接口,即接口可以多继承。
  • 一个接口继承多个接口,如果多个接口中存在方法签名冲突,则此时不支持多继承。
  • 一个类实现多个接口,如果多个接口中存在方法签名冲突,则此时不支持多实现。
  • 一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会优先用父类的。
  • 一个类实现了多个接口,多个接口中存在同名的默认方法,可以不冲突,这个类重写该方法即可。

6. 内部类

6.1 静态内部类

什么是静态内部类

有static修饰的内部类,属于外部类自己持有。

1
2
3
4
5
6
public class Outer {
// 静态内部类
public static class Inner {

}
}

创建对象的格式

1
2
外部类名.内部类名 对象名 = new 外部类.内部类(...);
Outer.Inner in = new Outer.Inner()

静态内部类中访问外部类成员的特点

可以直接访问外部类的静态成员,不可以直接访问外部类的实例成员。

局部内部类(了解即可)

局部内部类是定义在方法中、代码块中、构造器等执行体中的内部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {

}
public static void go() {
// 类
class A {}
// 抽象类
abstract class B {}
// 接口
interface C {}
}
}

6.2 匿名内部类

匿名内部类就是一种特殊的局部内部类,所谓匿名,指的是程序员不需要为这个类声明名字。

1
2
3
new 类或接口(参数值...) {
类体(一般是重写);
}
1
2
3
4
5
new Animal() {
@Override
public void cry() {
}
};

特点:匿名内部类本质就是一个子类,并会立即创建出一个子类对象。

作用:用于更方便地创建一个子类对象。

应用场景:匿名内部类通常作为一个参数传输给方法。

7. 枚举

7.1 认识枚举

枚举是一种特殊的Java类,格式如下:

1
2
3
public enum A {
X,Y,Z;
}

枚举类编译后,再反编译可以看到如下源码:

1
2
3
4
5
6
7
8
9
Compiled from "A.java"
public final class A extends java.lang.Enum<A> {
public static final A X = new A();
public static final A X = new A();
public static final A X = new A();

public static A[] values;
public static A valueOf(java.lang.String);
}

枚举类的特点

  • 枚举类的第一行只能罗列一些名称,这些名称都是常量,并且每个常量记住的都是枚举类的一个对象。
  • 枚举类的构造器都是私有的(写不写都只能是私有的),因此,枚举类对外不能创建对象。
  • 枚举都是最终类,不可以被继承。
  • 枚举类中,从第二行开始,可以定义类的其他各种成员。
  • 编译器为枚举类新增了几个方法,并且枚举类都是继承java.lang.Enum类的,从num类也会继承到一些方法。

7.2 枚举的作用

以下是枚举类的常见应用场景

  • 用来表示一组信息,然后作为参数传递,比如男女性别、一年四季等等常量。
  • 可以定义枚举表示一组信息并作为参数传输,使传入参数值内容时受到枚举类的约束。
  • 代码可读性好,参数值得到了约束,对使用者更友好,更建议使用。

8. 泛型

8.1 认识泛型

什么是泛型

定义类、接口、方法时,同时声明了一个或者多个类型变量(如:<E>),称为泛型类、泛型接口、泛型方法,它们统称为泛型。

1
2
3
public class ArrayList<E> {
...
}

泛型的作用

泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力。这样可以避免强制类型转换,以及可能出西安的异常。

泛型的本质

把具体的数据类型作为参数传递给变量,即类型参数化。

8.2 泛型类

8.3 泛型接口

8.4 泛型方法

8.5 泛型的注意事项