第一章 继承

1.1 概述

由来

多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那一个类即可。

定义

  • 继承:
    就是子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。

好处

  1. 提高代码的复用性。
  2. 类与类之间产生了关系,是多态的前提。

1.2 继承的格式

通过 extends 关键字,可以声明一个子类继承另外一个父类,定义格式如下:

  1. class 父类 {
  2. ...
  3. }
  4. class 子类 extends 父类 {
  5. ...
  6. }

1.3 继承后的特点——成员变量

1. 成员变量不重名

如果子类父类中出现不重名的成员变量,这时的访问是没有影响的。

2. 成员变量重名

如果子类父类中出现重名的成员变量,这时的访问是有影响的。会被子类的成员变量覆盖。

子父类中出现了同名的成员变量时,在子类中需要访问父类中非私有成员变量时,需要使用super 关键字,修饰父类成员变量,类似于之前学过的 this 。

小贴士:
Fu 类中的成员变量是非私有的,子类中可以直接访问。若Fu类中的成员变量私有了,子类是不能直接访问的。通常编码时,我们遵循封装的原则,使用private修饰成员变量,那么如何访问父类的私有成员变量呢?对!可以在父类中提供公共的getXxx方法和setXxx方法。

1.4 继承后的特点——成员方法

1. 成员方法不重名

如果子类父类中出现不重名的成员方法,这时的调用是没有影响的。对象调用方法时,会先在子类中查找有没有对应的方法,若子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法。

2. 成员方法重名——重写(Override)

如果子类父类中出现重名的成员方法,这时的访问是一种特殊情况,叫做方法重写 (Override)。

重写的应用

子类可以根据需要,定义特定于自己的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现父类方法,从而进行扩展增强。比如新的手机增加来电显示头像的功能。

注意事项
  1. 子类方法覆盖父类方法,必须要保证权限大于等于父类权限。
  2. 子类方法覆盖父类方法,返回值类型、函数名和参数列表都要一模一样。

1.5 继承后的特点——构造方法

首先我们要回忆两个事情,构造方法的定义格式和作用。

  1. 构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的。
  2. 构造方法的作用是初始化成员变量的。所以子类的初始化过程中,必须先执行父类的初始化动作。子类的构造方法中默认有一个super(),表示调用父类的构造方法,父类成员变量初始化后,才可以给子类使用。代码如下:
  1. class Fu {
  2. private int n;
  3. Fu(){
  4. System.out.println("Fu()");
  5. }
  6. }
  7. class Zi extends Fu {
  8. Zi(){
  9. // super(),调用父类构造方法
  10. super();
  11. System.out.println("Zi()");
  12. }
  13. }
  14. public class ExtendsDemo07{
  15. public static void main (String args[]){
  16. Zi zi = new Zi();
  17. }
  18. }
  19. 输出结果:
  20. Fu()
  21. Zi()

1.6 super和this

super和this的用法

  1. 访问成员
  1. this.成员变量 ‐‐ 本类的
  2. super.成员变量 ‐‐ 父类的
  3. this.成员方法名() ‐‐ 本类的
  4. super.成员方法名() ‐‐ 父类的
  1. 访问构造方法
  1. this(...) ‐‐ 本类的构造方法
  2. super(...) ‐‐ 父类的构造方法

子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。
super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。

第二章 抽象类

2.1 概述

由来

父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。

定义

抽象方法:没有方法体的方法。
抽象类:包含抽象方法的类。

2.2 abstract使用格式

抽象方法

使用abstract 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。

定义格式:
  1. 修饰符 abstract 返回值类型 方法名 (参数列表);
  2. public abstract void run();

抽象类

如果一个类包含抽象方法,那么该类必须是抽象类。

定义格式:
  1. abstract class 类名字 {
  2. }

抽象的使用

继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该父类的抽象方法,否则,从最初的父类到最终的子类都不能创建对象,失去意义。

2.3 注意事项

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  1. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。

理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。

  1. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  1. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。

理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

第三章 接口

3.1 概述

接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么接口的内部主要就是封装了方法,包含抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法(JDK 9)。
接口的定义,它与定义类方式相似,但是使用interface关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。

引用数据类型:数组,类,接口。

3.2 定义格式

  1. public interface 接口名称 {
  2. // 抽象方法
  3. // 默认方法
  4. // 静态方法
  5. // 私有方法
  6. }
  1. 抽象方法:使用abstract关键字修饰,可以省略,没有方法体。该方法供子类实现使用。
  2. 默认方法:使用default修饰,不可省略,供子类调用或者子类重写。
  3. 静态方法:使用static修饰,供接口直接调用。
  4. 私有方法:使用private修饰,供接口中的默认方法或者静态方法调用。

3.3 基本的实现

实现的概述

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements 关键字。

非抽象子类实现接口:

  1. 必须重写接口中所有抽象方法。
  2. 继承了接口的默认方法,即可以直接调用,也可以重写。

实现格式:

  1. class 类名 implements 接口名 {
  2. // 重写接口中抽象方法【必须】
  3. // 重写接口中默认方法【可选】
  4. }

抽象方法的使用

必须全部实现重写。如果抽象方法有重名的,只需要重写一次。

默认方法的使用

可以继承,可以重写,二选一,但是只能通过实现类的对象来调用。如果默认方法有重名的,必须重写一次。

静态方法的使用

静态与.class 文件相关,只能使用接口名调用,不可以通过实现类的类名或者实现类的对象调用。接口中,存在同名的静态方法并不会冲突,原因是只能通过各自接口名访问静态方法。

私有方法的使用

  • 私有方法:只有默认方法可以调用。
  • 私有静态方法:默认方法和静态方法可以调用。

如果一个接口中有多个默认方法,并且方法中有重复的内容,那么可以抽取出来,封装到私有方法中,供默认方法去调用。从设计的角度讲,私有的方法是对默认方法和静态方法的辅助。同学们在已学技术的基础上,可以自行测试。

第四章 多态

4.1 概述

定义

  • 多态: 是指同一行为,具有多个不同表现形式。

前提【重点】

  1. 继承或者实现【二选一】
  2. 方法的重写【意义体现:不重写,无意义】
  3. 父类引用指向子类对象【格式体现】

4.2 多态的体现

多态体现的格式:

  1. 父类类型 变量名 = new 子类对象;
  2. 变量名.方法名();

父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后方法。

4.3 多态的好处

实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。代码如下:

定义父类:

  1. public abstract class Animal {
  2. public abstract void eat();
  3. }

定义子类:

  1. class Cat extends Animal {
  2. public void eat() {
  3. System.out.println("吃鱼");
  4. }
  5. }
  6. class Dog extends Animal {
  7. public void eat() {
  8. System.out.println("吃骨头");
  9. }
  10. }

定义测试类:

  1. public class Test {
  2. public static void main(String[] args) {
  3. // 多态形式,创建对象
  4. Cat c = new Cat();
  5. Dog d = new Dog();
  6. // 调用showCatEat
  7. showCatEat(c);
  8. // 调用showDogEat
  9. showDogEat(d);
  10. /*
  11. 以上两个方法, 均可以被showAnimalEat(Animal a)方法所替代
  12. 而执行效果一致
  13. */
  14. showAnimalEat(c);
  15. showAnimalEat(d);
  16. }
  17. public static void showCatEat (Cat c){
  18. c.eat();
  19. }
  20. public static void showDogEat (Dog d){
  21. d.eat();
  22. }
  23. public static void showAnimalEat (Animal a){
  24. a.eat();
  25. }
  26. }

由于多态特性的支持,showAnimalEat方法的Animal类型,是Cat和Dog的父类类型,父类类型接收子类对象,当然可以把Cat对象和Dog对象,传递给方法。
当eat方法执行时,多态规定,执行的是子类重写的方法,那么效果自然与showCatEat、showDogEat方法一致,所以showAnimalEat完全可以替代以上两方法。
不仅仅是替代,在扩展性方面,无论之后再多的子类出现,我们都不需要编写showXxxEat方法了,直接使用showAnimalEat都可以完成。
所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。

4.4 引用类型转换

多态的转型分为向上转型与向下转型两种:

向上转型

  • 向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。

当父类引用指向一个子类对象时,便是向上转型。

使用格式:
  1. 父类类型 变量名 = new 子类类型();
  2. 如:Animal a = new Cat();

向下转型

  • 向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。

一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。

使用格式:
  1. 子类类型 变量名 = (子类类型) 父类变量名;
  2. 如:Cat c =(Cat) a;

为什么要转型

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点”小麻烦”。所以,想要调用子类特有的方法,必须做向下转型。

转型演示,代码如下:
定义类:

  1. abstract class Animal {
  2. abstract void eat();
  3. }
  4. class Cat extends Animal {
  5. public void eat() {
  6. System.out.println("吃鱼");
  7. }
  8. public void catchMouse() {
  9. System.out.println("抓老鼠");
  10. }
  11. }
  12. class Dog extends Animal {
  13. public void eat() {
  14. System.out.println("吃骨头");
  15. }
  16. public void watchHouse() {
  17. System.out.println("看家");
  18. }
  19. }

定义测试类:

  1. public class Test {
  2. public static void main(String[] args) {
  3. // 向上转型
  4. Animal a = new Cat();
  5. a.eat(); // 调用的是 Cat 的 eat
  6. // 向下转型
  7. Cat c = (Cat)a;
  8. c.catchMouse(); // 调用的是 Cat 的 catchMouse
  9. }
  10. }

转型的异常

转型的过程中,一不小心就会遇到这样的问题,请看如下代码:

  1. public class Test {
  2. public static void main(String[] args) {
  3. // 向上转型
  4. Animal a = new Cat();
  5. a.eat(); // 调用的是 Cat 的 eat
  6. // 向下转型
  7. Dog d = (Dog)a;
  8. d.watchHouse(); // 调用的是 Dog 的 watchHouse 【运行报错】
  9. }
  10. }

这段代码可以通过编译,但是运行时,却报出了 ClassCastException,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。

为了避免ClassCastException的发生,Java提供了 instanceof关键字,给引用变量做类型的校验,格式如下:

  1. 变量名 instanceof 数据类型
  2. 如果变量属于该数据类型,返回true。
  3. 如果变量不属于该数据类型,返回false。

第五章 final关键字

5.1 概述

学习了继承后,我们知道,子类可以在父类的基础上改写父类内容,比如,方法重写。那么我们能不能随意的继承API中提供的类,改写其内容呢?显然这是不合适的。为了避免这种随意改写的情况,Java提供了final关键字,用于修饰不可改变内容。

小贴士:可以用于修饰类、方法和变量。

1. 局部变量——基本类型

思考,如下两种写法,哪种可以通过编译?
写法1:

  1. final int c = 0;
  2. for (int i = 0; i < 10; i++) {
  3. c = i;
  4. System.out.println(c);
  5. }

写法2:

  1. for (int i = 0; i < 10; i++) {
  2. final int c = i;
  3. System.out.println(c);
  4. }

根据 final 的定义,写法1报错!写法2,为什么通过编译呢?因为每次循环,都是一次新的变量c。这也是大家需要注意的地方。

2. 局部变量——引用类型

引用类型的局部变量,被final修饰后,只能指向一个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改。

3. 成员变量

成员变量涉及到初始化的问题,初始化方式有两种,只能二选一:

  • 显示初始化;
  1. public class User {
  2. final String USERNAME = "张三";
  3. private int age;
  4. }
  • 构造方法初始化。
  1. public class User {
  2. final String USERNAME ;
  3. private int age;
  4. public User(String username, int age) {
  5. this.USERNAME = username;
  6. this.age = age;
  7. }
  8. }

被final修饰的常量名称,一般都有书写规范,所有字母都大写。

第六章 权限修饰符

6.1 概述

在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限,

  • public:公共的。
  • protected:受保护的
  • default:默认的
  • private:私有的

6.2 不同权限的访问能力

public protected default(空的) private
同一类中 √ √ √ √
同一包中 (子类与无关类) √ √ √
不同包的子类 √ √
不同包中的无关类 √

第七章 内部类

7.1 概述

什么是内部类

将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类。

定义格式:
  1. class 外部类 {
  2. class 内部类{
  3. }
  4. }

访问特点

  • 内部类可以直接访问外部类的成员,包括私有成员。
  • 外部类要访问内部类的成员,必须要建立内部类的对象。

创建内部类对象格式:
  1. 外部类名.内部类名 对象名 = new 外部类型().new 内部类型();

7.2 匿名内部类【重点】

  • 匿名内部类 :是内部类的简化写法。它的本质是一个带具体实现的父类或者父接口的匿名的子类对象。
    开发中,最常用到的内部类就是匿名内部类了。以接口举例,当你使用一个接口时,似乎得做如下几步操作,
  1. 定义子类
  2. 重写接口中的方法
  3. 创建子类对象
  4. 调用重写后的方法

我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。

前提

匿名内部类必须继承一个父类或者实现一个父接口。

格式

  1. new 父类名或者接口名(){
  2. // 方法重写
  3. @Override
  4. public void method() {
  5. // 执行语句
  6. }
  7. };

使用方式

以接口为例,匿名内部类的使用,代码如下:

定义接口:
  1. public abstract class FlyAble{
  2. public abstract void fly();
  3. }

创建匿名内部类,并调用:
  1. public class InnerDemo {
  2. public static void main(String[] args) {
  3. /*
  4. 1.等号右边:是匿名内部类,定义并创建该接口的子类对象
  5. 2.等号左边:是多态赋值,接口类型引用指向子类对象
  6. */
  7. FlyAble f = new FlyAble(){
  8. public void fly() {
  9. System.out.println("我飞了~~~");
  10. }
  11. };
  12. //调用 fly方法,执行重写后的方法
  13. f.fly();
  14. }
  15. }

通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。代码如下:

  1. public class InnerDemo2 {
  2. public static void main(String[] args) {
  3. /*
  4. 1.等号右边:定义并创建该接口的子类对象
  5. 2.等号左边:是多态,接口类型引用指向子类对象
  6. */
  7. FlyAble f = new FlyAble(){
  8. public void fly() {
  9. System.out.println("我飞了~~~");
  10. }
  11. };
  12. // 将f传递给showFly方法中
  13. showFly(f);
  14. }
  15. public static void showFly(FlyAble f) {
  16. f.fly();
  17. }
  18. }