(目录)


包(package)

包 (package) 是组织类的一种方式,使用包的主要目的是保证类的唯一性。也就是说,在同一个包底下,不能存在同名的类。相反,在不同的包底下,能够存在相同名字的类。那么与包有关的关键字有import与package 。


1.import关键字

我们都知道在Java中有许多现成的类放在不同的包底下提供给程序员使用。而类中有能够满足我们需求的实现方法。那么如何才能使用我们所需要的类呢?要使用import关键字来导入该类所在的包。例如:可以使用import java.util.Date 这种方式引入 java.util 这个包中的 Date 类。如果还需要使用java.util包下的其它类时,例如Scanner类,则还需要import java.util.Scanner

如果我们没有导入该包中的类,则还可以写成这种形式:java.util.Date date = new java.util.Date();但是这种写法比较麻烦一些, 仍然建议使用 import 语句导入包。如果我们不单单只想导入java.util当中的Date类,我们也可以写成import java.util. 号代表的是通配符,顾名思义就是能够将该包下的“所有的类”都导入,为什么“所有的类”加上引号呢?这里的“所有的类”指的是我们在程序中调用了java.util包下的类时无需再导java.util.类名,而import java.util.*这种写法能够在我们使用哪个类时就导入包中指定的哪个类,只是无需再导相同的包。但它却又不是将该包底下的所有类都导入进来。

还有特殊情况:因为在不同包底下可以有相同类名,因此如果真的遇到了**想调用不同包底下的相同的类时,两个包都要进行导入才能正常使用。**并且要看清楚编译器提示给我们的是哪个包底下的类。调用类的方法则是 类名.方法名


2.静态导入

使用 import static 可以导入包中的静态的方法和字段。这种情况用的比较少。例如System类中的方法println是静态方法,因此写成下面这种形式:

import static java.lang.System.*;
public class Test {
    public static void main(String[] args) {
        out.println("hello");
   }
}

上面的例子看的有些别扭,但是导入静态的方法有时也会非常方便,例如:

import static java.lang.Math.*;
public class Test {
    public static void main(String[] args) {
        double x = 30;
        double y = 40;
        // 静态导入的方式写起来更方便一些. 
        // double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        double result = sqrt(pow(x, 2) + pow(y, 2));
        System.out.println(result);
   }
}


3.package关键字

我们既然已经知道了import关键字的作用是导入一个包底下的类,而package关键字就是显示出该类是在哪个包底下的。

基本规则:

  1. 在文件的最上方加上一个 package 语句指定该代码在哪个包中
  2. 包名需要尽量指定成唯一的名字,通常会用公司的域名的颠倒形式(例如com.bit.demo1)
  3. 包名要和代码路径相匹配,例如创建 com.baidu.www 的包, 那么会存在一个对应的路径 com.baidu.www 来存储代码
  4. 如果一个类没有 package 语句, 则该类被放到一个默认包中

当我们创建了多个包时对于某一个类在哪个包底下会显得有点混乱,我们可以在该类的标签处点击鼠标右键后有如图所下的界面: image-20220104193401441

并且当创建时,编译器自动提示将该类在哪个包底下。

注意1:util包中 有很多类,难道 “ impor java.util.*; ” 是一下子全部 都导入吗?

不是的,Java 处理的时候,需要哪个类,它才会给你那个类。 就是说:Java不会提前给你任何类,当你需要某个类的时候,再给你。而且是要一个,给一个。 不会说要一个,给你一大堆。

这也是 Java 和 C 的区别之一。 假设 include<stdio.h> C 通过 include 关键字,将 stdio.h 头文件里面的内容全部都拿过来,导入程序。 这就好比 古时 朝廷 发赈灾款 给 某位大臣,这批钱,你怎么用都行。 而不是 先排大臣过去,碰到一项需要开支的地方,再向朝廷申请 合适的 拨款数目

如果你要问:Java 和 C 那个导入方式好,肯定是java。因为 Java 是用一个,给一个,用不到的,绝对不会 给你。省空间。 不像C一样,一股脑的全给你,管你用不用,这样就会造成空间的浪费,


注意二:

import java.util.*;

image-20220104193746327

但是有一个问题,通配符难道是一次性把所有的类都导入了吗?

不是,java 在处理中需要谁才导入谁

在通配符的使用过程中会出现一个问题

import java.util.*;
import java.sql.*;

image-20220104194415445

这两个类中都有 Date 包,编译器分辨不清,导致程序出错

这种情况下需要用完整的类名

java.util.Date date = new java.util.Date();

4.创建自己的包

基本规则

  1. 在文件的最上方加上一个 package 语句指定该代码在哪个包中.
  2. 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.bit.demo1 ).
  3. 包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码
  4. 如果一个类没有 package 语句, 则该类被放到一个默认包中

image-20220104194712266

注意:包名必须是小写的,不能是大写的


包的访问权限控制

我们已经了解了类中的 public 和 private. private 中的成员只能被类的内部使用.

如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用.

​ 什么情况可以称为包的访问权限:

当你的成员变量不加任何的访问修饰限定词的时候,默认就是一个包的访问权限,只可以在当前包中使用

比如:

image-20220104195118646

常见的包(系统包)

  1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
  2. java.lang.reflect:java 反射编程包;
  3. java.net:进行网络编程开发包。
  4. java.sql:进行数据库开发的支持包。
  5. java.util:是java提供的工具程序包。(集合类等) 非常重要
  6. java.io:I/O编程开发包。

面向对象的基本特征

1:继承 特殊类的对象拥有其一般类的全部属性与服务,称作特殊类对一般类的继承。例如,轮船、客轮;人、大人。一个类可以是多个一般类的特殊类,它从多个一般类中继承了属性与服务,这称为多继承。例如,客轮是轮船和客运工具的特殊类。在java语言中,通常我们称一般类为父类(superclass,超类),特殊类为子类(subclass)。但我们要注意java语言是不支持多继承的,它对多继承的实现是通过接口来进行的。

子类继承父类的:大部分成员变量和大部分成员方法(不包括私有变量和私有方法)

2:抽象类和抽象方法 类中有一个abstract修饰符(但要注意只要有抽象方法的类就是抽象类)、父类中存在抽象方法(也可以没有)抽象方法没有方法体并且修饰符一定要是public或者protected,父类不能new出来 所有子类都必须实现这些抽象方法(如果没实现就必须把它也定义成一个抽象方法) 虽然不能new出来但是它可以有自己的static方法。

3:封装

某个类具有很多的方法,很多时候只管调用,不必了解具体实现,我们只要具体的结果这种是典型的“结果导向” 封装有两个含义

  1. 把对象的全部属性和全部服务结合在一起,形成一个不可分割的独立单位(即对象)。
  2. 信息隐蔽,即尽可能隐蔽对象的内部细节,对外形成一个边界〔或者说形成一道屏障〕,只保留有限的对外接口使之与外部发生联系。

封装的优点:

封装的原则在软件上的反映是:要求使对象以外的部分不能随意存取对象的内部数据(属性),从而有效的避免了外部错误对它的”交叉感染”,使软件错误能够局部化,大大减少查错和排错的难度。

4:接口实现多继承 在类中实现接口可以使用关键字implements 在类的继承中,只能做单重继承,而实现接口时,一次则可以实现多个接口, 每个接口间使用逗号“,”分隔。

多态三个条件(继承、重写、父类对象引用指向子类对象)

多态的好处: 1.可替换性(substitutability)。多态对已存在代码具有可替换性。 2.可扩充性(extensibility)。多态对代码具有可扩充性。 3.接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。 4.灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。 5.简化性(simplicity)。多态简化对应用软件的代码编写和修改过程。


一、继承

(1)什么是继承

代码中如果出现“重复代码”往往意味着一定的风险,当我们要修改这段“重复代码”时,可能要修改多处,造成代码的维护性下降。

为了避免这件事情,可以使用面向对象中的一个重要用法——继承

继承的目的是代码重用,类的重用。把多个类之间的共同代码(共同特性)提取出来,放到"父类"中,然后再由各个子类分别继承这个父类,子类会拥有父类的属性和方法,从而就可以把重复代码消灭了。使用的继承关键字是extends

被继承的称为父类/基类/超类,继承的类称为子类/派生类。


(2)语法规则

使用关键字 extern 进行处理,意义:可以对代码重用

代码举例:

class Animal{
    public int age;
    public String name;
    public void eat(){
        System.out.println("eat()");
    }
}
class Dog extends Animal{
    
}
class Bird extends Animal{
    public String wing;
    public void fly(){
        System.out.println("fly()");
    }
}
public class TestDemo1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        System.out.println(dog.name);
    }

}

↑↑↑↑ Dog 和 Bird 继承了 Animal 的属性(两种的共同属性)

仔细分析, 我们发现 Animal 和 Dog t以及 Bird 这几个类中存在一定的关联关系: ↓↓↓↓

image-20220104200434107

  1. 这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的.
  2. 这三个类都具备一个相同的 name 属性, 而且意义是完全一样的.
  3. 从逻辑上讲, Cat 和 Bird 都是一种 Animal(is - a 语义)

继承中的 Dog(Bird) 可以被称为子类/派生类,Animal 可以称为父类/基类/超类,通过 extern 可以继承父类的属性(和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果)

image-20220104200340992

由于 Dog (Bird) 继承了父类 Animal 的属性,所以这里可以通过 (.)访问父类的属性

public static void main(String[] args) {
        Dog dog = new Dog();
        System.out.println(dog.name);
    }

例如:

image-20220104200629380


注意:

  • 使用 extends 指定父类.
  • Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).
  • 子类会继承父类的所有 public 的字段和方法.
  • 对于父类的 private 的字段和方法, 子类中是无法访问的.
  • 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用
  • 子类在构造的时候,要先帮助父类构造

看最后一个注意事项,当为父类提供一个构造方法时,程序会报错 !!!

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

报错 error

image-20220104201408442

解决方式:子类帮助父类构造,即(子类先调用父类的构造方法

class Dog extends Animal{
    public Dog(String name, int age){
        super(name,age);//调用父类方法
    }
}

使用 super 关键字,先行调用父类,super 为显示调用构造方法

image-20220104201637863

那为什么不写构造方法的时候程序不会报错???

因为当不写构造方法的时候,编译器默认生成一个没有参数的构造方法

如下方的构造方法:

public Dog(){
        super();
    }

super 用法总结:

  • super(); 调用父类的构造方法
  • super.func(); 调用父类的方法
  • super.name; 调用父类的成员属性
  • 不能出现在静态方法中,它是依赖对象的
  • super 一定出现在构造方法的第一行,其他方法中不可以

子类继承父类,子类在内存中的情况

image-20220104202108064


(3)访问权限关键字

访问权限图( private | default | protected | public)

No 范围 private default(包权限) protected public
1 同一个包中的同类 ok ok ok ok
2 同一个包中的不同类 no ok ok ok
3 不同包中的子类 no no ok ok
4 不同包中的非子类 no no no ok

1.同一个包中的同一个类:

image-20220104202503049

2.同一个包中的不同类

image-20220104202526077

3.不同包的子类(类的继承需要两个类都是public的类

image-20220104202606065

4.不同包的非子类

image-20220104202637294


注意事项:

  1. 我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.
  2. 因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public.
  3. 另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 还是更希望大家能写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用).

(4)final 关键字

如果一个类不想被继承,可以把该类设置为被 final 修饰

总结 final 修饰:

  • final 修饰常量,不可被修改
  • final 修饰类,不可被继承
  • final 修饰方法,方法不可重写,属性不可覆盖

1.final关键字修饰常量:常量不可被修改,并且需要赋初值才能使用。例:

final int a = 10; 
a = 20; // 编译出错

2.final关键字修饰类:这个类不能被继承。

final public class Animal { 
 ... 
} 
public class Bird extends Animal { 
 ... 
} 
// 编译出错,Animal无法被继承

我们经常使用的String字符串类就是被final修饰的类,因此它无法被继承。

image-20220104203158974

3.final关键字修饰方法:方法不能够被重写。

public class Animal { 
    final public void fun() {
    }
} 
public class Bird extends Animal { 
    public void func() {
    }
} 
// 编译出错,func方法无法被重写

二、组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.这是我们设计类的一种常用方式之一.

  • 组合表示 has - a 语义

在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师.

  • 继承表示 is - a 语义

在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物.

例如:

①学校包含两个学生和两个老师

class Teacher{
    String name;
    String subject;
}
class Student{
    String name;
    String num;
}
public class School{
    public Teacher[] teacher=new Teacher[2];
    public Student[] student=new Student[2];
}

②圆中包含原点和半径

class Point{
    
}
class Raduis{
    
}
public class Circle {
    Point point=new Point();
    Raduis raduis=new Raduis();
}

三、多态

多态(字面意思):一种事物多种形态

多态中有三种重要的语法基础:向上转型、动态绑定、重写。缺一不可~

理解多态就需要理解:向上转型,即(父类对象引用子类对象)

(1)向上转型

public static void main(String[] args) {
        Animal animal = new Dog("haha",19);
}

什么情况下会发生向上转型

  1. 直接赋值

image-20220104204952613

image-20220104205030822

  1. 作为函数的参数

image-20220104205057884

  1. 作为函数的返回值

image-20220104205126738


(2)动态绑定

动态绑定的条件:

  1. 父类 引用 子类的对象

  2. 通过父类这个引用 调用 父类 和 子类 同名的覆盖(重写) 方法

重写条件:

  1. 方法名相同

  2. 参数的 个数 和 类型 相同

  3. 最好返回值相同(协变类型:返回值可以不同。返回值的关系为父子类关系)

  4. 父子类的关系

注:如果父类中包含的方法在子类中有对应的同名同参数的方法,就会进行动态绑定。由运行时决定调用哪个方法。

一般动态/静态分别指的是编译时/运行时,和static无关。

示例

public static void main(String[] args) {
        Animal animal = new Dog("haha",19);
        animal.eat();
    }

eat()

我们可以看到这里调用的时父类的方法

再看一个代码: ↓↓↓(当子类中也有同名的 eat()方法)

class Dog extends Animal{
    public Dog(String name, int age){
        super(name,age);
    }
    public void eat(){
        System.out.println("狼吞虎咽的eat()");
    }
}
 public static void main(String[] args) {
        Animal animal = new Dog("haha",19);
        animal.eat();
    }

狼吞虎咽的eat()

我们看到这里调用的是子类的方法,这是为什么呢

因为这里发生了动态绑定

利用 javap -c打开文件的汇编代码,可以看到这里调用的还是 Animal 的 eat 方法,这是为什么呢?

image-20220104220258427

在编译的时候不能够确定此时到底调用谁的方法,在运行的时候才知道调用谁的方法,称其为运行时绑定--------即我们的动态绑定

动态绑定的两个前提:

  1. 父类引用 引用子类对象
  2. 通过这个父类引用调用父类和子类同名的覆盖方法

那么什么是同名的覆盖

👉 同名的覆盖又被叫做重写重写要满足以下几种情况

  1. 方法名相同
  2. 参数列表相同(个数+类型)
  3. 返回值相同
  4. 父子类的情况下

重写 与 重载的区别

  1. 方法名相同

  2. 参数的 个数 和 类型 必须有一个不同(重写:参数的类型和个数都相同)

  3. 返回值可以不同 (重写:协变类型可以返回值不同,一般情况返回值是一样)

  4. 重载涉及静态绑定(重写:动态绑定)

重写的注意事项:

  1. 方法不可以是静态的,静态的方法不可以重写
  2. 子类的访问修饰限定符范围一定要大于等于父类的访问修饰限定符
  3. private 方法不能重写
  4. 被 final 修饰的(关键字/方法)不可以被重写
  5. 协变类型也可以构成重写

协变类型(科普一下)

public Animal eat(){
        System.out.println("eat()");
        return null;
    }
    public Dog eat(){
        System.out.println("狼吞虎咽的eat()");
        return null;
    }

在父类引用子类的时候有一个注意事项:

image-20220104220649547

通过父类引用只可以访问父类自己的成员


(3)静态绑定

静态绑定:根据你给定的参数个数和类型,判断调用哪个方法(又被称为编译式多态)

动态绑定也称为运行时绑定。静态绑定也有编译时绑定的说法。为什么会有这种说法呢?

class Bird extends Animal{
    public Bird(String name, int age,String wing){
        super(name,age);
    }
    public String wing;
    public void fly(){
        System.out.println("fly()");
    }
    public void func(int a){
        System.out.println(a);
    }
    public void func(int a,int b){
        System.out.println(a);
    }
    public void func(int a,int b,int c){
        System.out.println(a);
    }
}


 public static void func(Animal animal){
        Bird bird = new Bird("haha",19,"fei");
        bird.func(10);
    }

我们看他的汇编代码:

image-20220104221140706

我们可以看到这里在编译的时候已经规定好了调用哪个 func 方法,这就是静态绑定


(5)向下转型

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象。相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途。

编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法。 虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的。

// (Bird) 表示强制类型转换
Bird bird = (Bird)animal; 
bird.fly(); 
// 执行结果
圆圆正在飞

image-20220104222916473

但是这样的向下转型有时是不太可靠的,也不太安全。

Animal animal = new Cat("小猫"); 
Bird bird = (Bird)animal; 
bird.fly(); 
// 执行结果, 抛出异常
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird at Test.main(Test.java:35)

animal 本质上引用的是一个 Cat 对象, 是不能转成 Bird 对象的,运行时就会抛出异常,所以,为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换。

Animal animal = new Cat("小猫"); 
if (animal instanceof Bird) { 
 Bird bird = (Bird)animal; 
 bird.fly(); 
}

instanceof 可以判定一个引用是否是某个类的实例,如果是, 则返回 true。这时再进行向下转型就比较安全了。


(4)使用多态的好处

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.

我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况

利用一个代码加以说明

class Shape{
    public void draw(){
        System.out.println();
    }
}
class sanjiao extends Shape{
    @Override
    public void draw() {
        System.out.println("△");
    }
}
class fangpian extends Shape{
    @Override
    public void draw() {
        System.out.println("♦");
    }
}
//分割线//
public class Test {
    public static void drawmap(Shape shape){
        shape.draw();
    }

    public static void main(String[] args) {
        drawmap(new fangpian());
        drawmap(new sanjiao());
    }
}

在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的,

当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为多态

使用多态的好处是什么?

  1. 类调用者对类的使用成本进一步降低. 封装是让类的调用者不需要知道类的实现细节. 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可. 因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低. 这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷.

  2. 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else 例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下↓↓↓

  3. 可扩展能力更强.

public static void drawShapes() { 
 Rect rect = new Rect(); 
 Cycle cycle = new Cycle(); 
 Flower flower = new Flower(); 
 String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"}; 
 
 for (String shape : shapes) { 
 if (shape.equals("cycle")) { 
 cycle.draw(); 
 } else if (shape.equals("rect")) { 
 rect.draw(); 
 } else if (shape.equals("flower")) { 
 flower.draw(); 
 } 
 } 
} 

如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单,利用 foreach 进行打印

public static void drawShapes() { 
 // 我们创建了一个 Shape 对象的数组. 
 Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), 
 new Rect(), new Flower()}; 
 for (Shape shape : shapes) { 
 shape.draw(); 
 } 
} 

什么叫 “圈复杂度”

圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需 进行重构

为什么拓展更好?

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低. 而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高


※多态使用案例※

案例(使用多态,打印多种形状)

代码1:

class Shape{// 无论是 三角形,还是正方形等等,它们都是图形
    // 以此作为共性抽出
    public void draw(){
        System.out.println("Shape::draw()");
    }
}
// 矩形
class Rect extends Shape{
    @Override// 当我们在子类中,重写了父类中的方法。那么父类方法中的输出语句就显得毫无意义
    // 最后都是输出子类draw方法,如果你想的话,可以删掉父类的 输出语句
    public void draw(){
        System.out.println("♦");
    }
}
// 花
class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("❀");
    }

image-20220104215230366


代码2(实现多态)

class Shape{// 无论是 三角形,还是正方形等等,它们都是图形
    // 以此作为共性抽出
    public void draw(){
        System.out.println("Shape::draw()");
    }
}
// 矩形
class Rect extends Shape{
    @Override// 当我们在子类中,重写了父类中的方法。那么父类方法中的输出语句就显得毫无意义
    // 最后都是输出子类draw方法,如果你想的话,可以删掉父类的 输出语句
    public void draw(){
        System.out.println("♦");
    }
}
// 花
class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("❀");
    }
}

public class Test {
    public static void main(String[] args) {
        Rect rect = new Rect();
        rect.draw();// 这里是单纯,new对象,访问普通成员方法

        // 使用向上转型,重写,父类当中 draw 方法。
        // 使父类的draw有了另一种实现的方法,
        // 这种情况,被称为动态绑定。
        // 在不断重写父类draw方法,呈现draw方法的多态的实现
        // 这就是我们所说的多态
        Shape shape = new Rect();
        shape.draw();
        Shape shape1 = new Flower();
        shape1.draw();
    }

}

image-20220104215736787

代码三,通过方法和向上转型,来实现多态。让你们更加直观

代码如下

class Shape{// 无论是 三角形,还是正方形等等,它们都是图形
    // 以此作为共性抽出
    public void draw(){
        System.out.println("Shape::draw()");
    }
}
// 矩形
class Rect extends Shape{
    @Override// 当我们在子类中,重写了父类中的方法。那么父类方法中的输出语句就显得毫无意义
    // 最后都是输出子类draw方法,如果你想的话,可以删掉父类的 输出语句
    public void draw(){
        System.out.println("♦");
    }
}
// 花
class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("❀");
    }
}

public class Test {
    public static void paint(Shape shape){
        shape.draw();
    }
    public static void main(String[] args) {
        Rect rect = new Rect();
        paint(rect);
        paint(new Rect());
        System.out.println("=============");
        Flower flower = new Flower();
        paint(flower);
        paint(new Flower());
    }
 }

image-20220104215909327

从另一个方面来说:通过一个引用来调用不同的draw方法,会呈现出不同的表现形式。表现的形式取决于将来它引用那个对象。这就是动态。而且实现多态的大前提,就是一定要向上转型,且实现 父类和子类的重写方法。


总结:

在这个代码中, 上方的代码(矩形、花、继承)是 类的实现者 编写的, 下方的代码(main所在的类)是 类的调用者 编写的。 当类的调用者在编写 Paint 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现。(和 shape 对应的实例相关), 这种行为就称为 多态。 ==多态 顾名思义, 就是 “一个引用, 能表现出多种不同形态”。==

多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带来的编码上的好处. 另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 "继承" 这样的语法并没有必然的联系. C++ 中的 "动态多态" 和 Java 的多态类似. 但是 C++ 还有一种 "静态多态"(模板), 就和继承体系没有关系了. Python 中的多态体现的是 "鸭子类型", 也和继承体系没有关系. Go 语言中没有 "继承" 这样的概念, 同样也能表示多态. 无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要 方式.


重写方法中的一个大坑

在构造方法中调用重写的方法(一个坑)

一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func

class B { 
 public B() { 
 // do nothing 
 func(); 
 } 
 public void func() { 
 System.out.println("B.func()"); 
 } 
} 
class D extends B { 
 private int num = 1; 
 @Override 
 public void func() { 
 System.out.println("D.func() " + num); 
 } 
} 
public class Test { 
 public static void main(String[] args) { 
 D d = new D(); 
  } 
} 
// 执行结果
D.func() 0
  • 构造 D 对象的同时, 会调用 B 的构造方法.
  • B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
  • 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.

结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.


抽象类

(1)什么是抽象类

抽象的反义词是具体,越不具体,就越抽象。abstract修饰的类就叫做抽象类,除了不能被实例化之外!其他语法规则和普通类都一样。

image-20220105130334482

abstract关键字存在的意义,就是让程序员明确的告诉编译器,这个类是一个抽象的类,不应该进行实例化,于是编译器就要做好相关检查工作。


(2)什么是抽象方法

  • 方法前头加上 abstract 此时这就是一个抽象方法了.
  • 抽象方法不需要方法体.
  • 抽象方法只能在抽象类中存在(也可以在接口中存在), 不能在普通的类中存在.
  • 抽象方法存在的意义就是为了让子类进行重写.

(3)抽象类中的规则及注意事项

abstract class Shape { 
 abstract public void draw(); 
}
  1. 抽象类不能够被实例化 例如上面的Shape类,不能写为下面这种形式:但是可以向上转型。
Shape shape = new Shape(); 
// 编译出错
Error:(30, 23) java: Shape是抽象的; 无法实例化
  1. 抽象方法不能是 private 的 如果一个方法被private修饰,则这个方法不能再被private修饰。
abstract class Shape { 
 abstract private void draw(); 
} 
// 编译出错
Error:(4, 27) java: 非法的修饰符组合: abstract和private
  1. 抽象类中可以包含其他的非抽象方法, 也可以包含字段。这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用。
abstract class Shape { 
 abstract public void draw(); 
 void func() { 
 System.out.println("func"); 
 } 
} 
class Rect extends Shape { 
 ... 
} 
public class Test { 
 public static void main(String[] args) { 
 Shape shape = new Rect(); 
 shape.func(); 
 } 
} 
// 执行结果
func
  1. 如果一个普通类继承了一个抽象类,则这个普通类需要重写这个抽象类所有的抽象方法。除非普通类也加上abstract才不用重写抽象父类的所有抽象方法。
abstract class Shape {
    abstract public void func();
}
class Circle extends Shape{
    public void func() {
        
    }
}

image-20220105131621954

  1. 在4的基础上,如果已经有了一个抽象类(子类)继承了一个抽象类(父类),此时还有一个普通类继承抽象类(子类)时,需要重写抽象类(子类)和抽象类(父类)的所有抽象方法。

image-20220105131343581

image-20220105131357104

  1. 抽象方法与抽象类都不能被final修饰。

image-20220105131243615

既然知道了 抽象类不能被 final修饰的原理,那么由此推论出:抽象方法也不可以被 final修饰。.

image-20220105131215584


总结抽象类:

  1. 包含抽象方法的类,叫做抽象类
  2. 什么是抽象方法,即没有具体实现的方法,被 abstract 修饰
  3. 抽象类不可以实例化
  4. 由于不能被实例化,所以抽象类只能被继承(最大的作用)
  5. 抽象类当中也可以包括和普通一样的成员和方法(静态的也可以)
  6. 一个普通类继承了一个抽象类,那么这个普通类需要重写抽象类的所有抽象方法
  7. 一个抽象类A如果继承一个抽象类B,那么这个抽象类A可以不实现抽象父类B的抽象方法
  8. 结合第7点当A类被一个普通类继承后,A和B这两个抽象类当中的抽象方法必须被重写
  9. 抽象类不能被 final 修饰,抽象方法也不可以被 final 修饰

接口

(1)什么是接口

我们知道,类之间只能单继承。为了实现类似“多继承”的效果,所以就引入了接口。接口是抽象类的更近一步,比抽象类还抽象。抽象类只是不能实例化,但是其他各个方面都和普通类差不多,接口就更抽象了,不光不能实例化,同时也不具备类的各种特性。

命名:接口的命名一般以大写字母I作为前缀,一般使用形容词进行命名。表示的语义:一个类具有XXX特性


(2)语法规则

接口的注意事项:

  1. 接口是用 interface 来修饰的
  2. 接口当中的普通方法不能有具体的实现,非要实现需要加关键字 default(不可以被重写)
  3. 接口中可以有静态方法
  4. 里面的所以方法都是 public 的
  5. 抽象方法默认是一个 public abstract 的
  6. 接口不可以被通过关键字 new 来进行实例化
  7. 类和接口之间的关系是通过 implements 来实现的
  8. 当一个类实现了一个接口那么就必须重新接口当中的抽象方法
  9. 接口中的成员变量默认是被 public static final 所修饰的,一定在定义的时候初始化
  10. 当一个类实现一个接口之后,重写这个方法,这个方法前面必须加上 pubilc
  11. 一个类可以通过通过关键字 static 继承一个抽象类或一个普通类,但是只能继承一个类,也可以通过 implements 来实现多个接口,接口之间使用(,)隔开
  12. 接口B和接口C可以使用 extends 关键字来进行操作,此时译为:拓展,C接口通过 extends 拓展B的功能,当一个类 D 通过 implements 来实现这个接口 B 的时候,不仅需要重写 B 的抽象方法,还有重写从C拓展的抽象方法

举例:

①接口中只能放抽象方法,不能放普通方法(下图中的波浪线~)

image-20220105132256838

接口中的抽象方法可以不写public abstract关键字,写或者不写,都表示抽象的共有的方法~

但是抽象类中的抽象方法必须得写abstract关键字,抽象类中除了可以放抽象方法还可以放普通方法,所以不写得话就表示一个普通方法,就得加上方法体了。


②接口中只能放public static final修饰的属性,不能放普通的属性

interface IEating {
    public static final String food = "fish";
}

③接口和类之间不能继承,只能是类实现了(implements)某个接口。接口和接口之间可以继承。

Animal类

public class Animal {
    
}

IEating接口

public interface IEating {
    public static final String food = "fish";
    public abstract void eat();
}

IJump接口

public interface IJump {
    void jump();
}

Cat类继承自Animal类,实现了IEating,IJump接口

public class Cat extends Animal implements IEating,IJump{
    @Override
    public void eat() {
 
    }
    @Override
    public void jump() {
 
    }
}

注意:

  • 实现的类必须重写接口中所有的方法。
  • 实现多个接口,接口之前使用逗号分割。

(3)实现多个接口

有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的,然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果。 现在我们通过类来表示一组动物

class Animal { 
 protected String name; 

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

另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, "会游泳的”

interface IFlying { 
 void fly(); 
} 
interface IRunning { 
 void run(); 
} 
interface ISwimming { 
 void swim(); 
}

接下来我们创建几个具体的动物。

class Cat extends Animal implements IRunning { 
 public Cat(String name) { 
 super(name); 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在用四条腿跑"); 
 } 
}

鱼, 是会游的

class Fish extends Animal implements ISwimming { 
 public Fish(String name) { 
 super(name); 
 } 
 @Override 
 public void swim() { 
 System.out.println(this.name + "正在用尾巴游泳"); 
 } 
}

青蛙, 既能跑, 又能游(两栖动物)

class Frog extends Animal implements IRunning, ISwimming { 
 public Frog(String name) { 
 super(name); 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在往前跳"); 
 } 
 @Override 
 public void swim() { 
 System.out.println(this.name + "正在蹬腿游泳"); 
 } 
} 
//提示, IDEA 中使用 ctrl + i 快速实现接口
//还有一种神奇的动物, 水陆空三栖, 叫做 "鸭子"
class Duck extends Animal implements IRunning, ISwimming, IFlying { 
 public Duck(String name) { 
 super(name); 
 } 
 @Override 
 public void fly() { 
 System.out.println(this.name + "正在用翅膀飞"); 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在用两条腿跑"); 
 } 
 @Override 
 public void swim() { 
 System.out.println(this.name + "正在漂在水上"); 
 } 
}

上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口。 继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性。

有了接口之后,我们可以也可以实现多态。 例如, 现在实现一个方法, 叫 “散步”。

public static void walk(IRunning running) { 
 System.out.println("我带着伙伴去散步"); 
 running.run(); 
}

参数可以不是 “动物”, 只要会跑,就能够实现多态。

class Robot implements IRunning { 
 private String name; 
 public Robot(String name) { 
 this.name = name; 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在用轮子跑"); 
 } 
} 
Robot robot = new Robot("机器人"); 
walk(robot); 
// 执行结果
我带着伙伴去散步
机器人正在用轮子跑

(4)接口间的继承

接口继承的关键字是extends,如果一个类只实现了其中的一个接口,那么而那个接口又继承了其它的接口,那么那个类就需要重写实现的接口及其继承的接口的所有方法。

interface IRunning { 
 void run(); 
} 
interface ISwimming { 
 void swim(); 
} 
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming { 
} 
class Frog implements IAmphibious { 
 ... 
}

通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类, 就继续要实现 run 方法,也需要实现 swim 方法。接口间的继承相当于把多个接口合并在一起,意为拓展。


(5)抽象类和接口的对比

抽象类和普通类差不多,只是不能实例化。而接口和普通的类之间相去甚远(包含的属性,方法,和其它类的关系) 一个类只能继承自一个抽象类,但是一个类可以同时实现多个接口。

为啥要发明接口这样的语法?

解决Java中不能多继承的问题。Java中的继承是单继承,有些场景下多继承是有用的。Java中可以通过继承一个类,实现多个接口的方式来完成类似于多继承的效果。


接口的使用实例

1.Comparable接口

以下用接口给对象数组进行排序。 给定一个学生类

class Student { 
 private String name; 
 private int score; 
 public Student(String name, int score) { 
 this.name = name; 
 this.score = score; 
 } 

 @Override 
 public String toString() { 
 return "[" + this.name + ":" + this.score + "]"; 
 } 
}

再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序)

Student[] students = new Student[] { 
 new Student("张三", 95), 
 new Student("李四", 96), 
 new Student("王五", 97), 
 new Student("赵六", 92), 
};

按照我们之前的理解, 数组我们有一个现成的 sort 方法。但是sort方法无法知道此时Student类型是按照什么排序的。名字?分数还是年龄?

Arrays.sort(students); 
System.out.println(Arrays.toString(students)); 
// 运行出错, 抛出异常. 
Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to 
java.lang.Comparable

所以这里需要我们告诉 Arrays.sort 以怎样的方式去进行比较

利用 Comparable 接口来让 Arrays.sort 知道比较的是学生

class Student implements Comparable<Student>

此时引入Comparable 接口, 并实现其中的 compareTo 方法。

重写 compareTo 方法后 Arrays.sort 就知道了是按照什么去排序的,就可以进行排序了

class Student implements Comparable { 
 private String name; 
 private int score; 
 public Student(String name, int score) { 
 this.name = name; 
 this.score = score; 
 } 
 @Override
  public String toString() { 
 return "[" + this.name + ":" + this.score + "]"; 
 } 
 @Override 
 public int compareTo(Object o) { 
 Student s = (Student)o; 
 if (this.score > s.score) { 
 return -1; 
 } else if (this.score < s.score) { 
 return 1; 
 } else { 
 return 0; 
 } 
 } 
}

在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象。也可以在接口后面加表明比较的是Student类中的某个属性。按alt+insert重写接口当中的compareTo方法。 然后比较当前对象和参数对象的大小关系(按分数来算)。

  • 如果当前对象应排在参数对象之前, 返回小于 0 的数字;
  • 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
  • 如果当前对象和参数对象不分先后, 返回 0;

也可以直接return this.score-o.score ; 大于0则升序,小于0降序,等于0不排序。 此时执行代码运行结果:

public int compareTo(Student o) {
       return this.age - o.age;
   }
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]

注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则。如果比较的是name属性,则需要this.name.compareTo(o.name) 。


2.Comparator接口

但是Comparable接口有一个缺点,它的可入侵性非常强,假设我们要求用名字去排序,必须在原来的基础上去改变。当其他人要使用这个方法时却不知道方法内部已经被改了,会造成很多麻烦。

因此还有个接口也是对自定义类型作比较的。它是Comparator接口当一个类实现了Comparator接口就需要重写compare方法。因此对于多个变量属性可以分成多个不同的类来实现compare方法。例如:在上面代码的基础上改为:

class Student {
    public String name ;
    public int age ;
    public double score ;
    Student(String name,int age,double score) {
        this.name=name;
        this.age=age;
        this.score=score;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}

//比较类
class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age-o2.age;
    }
}
class ScoreComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return (int)(o1.score-o2.score);
    }
}
class NameComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}

class TestDemo {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        Student student1 = new Student("zjr",18,99);
        Student student2 = new Student("zjj",100,23);
        Student student3 = new Student("zjd",23,45);
        students[0]=student1;
        students[1]=student2;
        students[2]=student3;
        ScoreComparator scoreComparator = new ScoreComparator();
        AgeComparator ageComparator = new AgeComparator();
        NameComparator nameComparator = new NameComparator();
        Arrays.sort(students,nameComparator);
        System.out.println(Arrays.toString(students));
    }

这样写的好处是:更灵活,对类的侵入性很小

这两个接口的使用,取决业务,但一般推荐比较器


3.Cloneable接口

对于一个自定义类型的拷贝,我们无法用 引用.clone() 方法直接进行拷贝,那么如何做到拷贝出一个新的引用并且改变原来的对象不改变新拷贝出来的对象。这种拷贝也称为深拷贝。运行Cloneable接口。

当我们实现Cloneable接口时按住ctrl键点入Cloneable接口时,我们发现是一个空接口,也称为标记接口,里面没有任何的字段和方法。标记接口的作用是说明一个类如果实现了Cloneable接口时就标明这个类是可以被克隆的。

下面的代码就能够做到对一个自定义类型进行拷贝。

class Person implements Cloneable{
    public int age;
    Student student = new Student();
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Test {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person();
        Person person2 = (Person)person1.clone();
        System.out.println(person1.age);
        System.out.println(person2.age);
        System.out.println("===========");
        person2.age=99;
        System.out.println(person1.age);
        System.out.println(person2.age);
    }
 }

代码运行结果:

在这里插入图片描述

实现Cloneable接口进行拷贝有几个需要注意的点(此时单纯拷贝一个引用):

  1. 第一步:要实现Cloneable接口。
  2. 第二步:对Cloneable接口中的clone()方法进行重写。(按alt键+insert键会有提示)

image-20220105134622597

main方法中如果是第一次.clone()后也会报错,此时按alt键+enter键有以下页面提示,选中第一个即可。

image-20220105134639772

对于在main方法中将一个克隆后的引用强转为自定义类型是因为我们重写的方法当中返回值是Object类型。

如果一个类当中又实例化了另一个类的对象。那么我们又需要写为下面这种形式,代码示例:

class Student implements Cloneable{
    public double money = 10.0;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
class Person implements Cloneable{
    public int age;
    Student student = new Student();
    @Override
    protected Object clone() throws CloneNotSupportedException {
        //return super.clone();
        Person p = (Person)super.clone();
        p.student= (Student)this.student.clone();//对当前对象的student引用进行拷贝
        return p ;
    }
}
public class Test {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person();
        Person person2 = (Person)person1.clone();
        System.out.println(person1.student.money);
        System.out.println(person2.student.money);
        System.out.println("===================");
        person2.student.money=99.9;
        System.out.println(person1.student.money);
        System.out.println(person2.student.money);
    }
}

代码运行结果: 在这里插入图片描述


此时最好利用图解来帮助理解。 主要针对的是下面这行代码:

    Person p = (Person)super.clone();
    p.student= (Student)this.student.clone();
    return p ;

这个方法是重写父类clone()方法的。理解它很重要。我们先用一个引用来接收父类克隆出来的引用,再用当前student引用来接收当前对象的student的引用,说明已经完全克隆完成,此时直接返回克隆出来的引用即可。此过程就是类似于main函数中的克隆过程。

此时最好用图来理解:

image-20220105135051056

由此我们可以发现Cloneable接口实现的是深拷贝。


面试中Java三大特性封装、继承、多态与抽象类、接口问题

面试问题1:普通类和抽象类的区别:

答:抽象类是由abstract关键字修饰的。在抽象类中被abstract修饰的方法可以不用实现。抽象类主要是用来被继承的。如果普通子类继承了抽象类则需要重写抽象类中的abstract方法。被absract修饰的方法不能同时被final与private修饰。其余的字段与方法都与普通类的相同。

面试问题2:接口和抽象类的区别:

接口中的只有常量,默认被public static final修饰,而方法都默认被public abstract修饰。而抽象类中的抽象方法需要手动添加abstract。接口中所有的方法都需要被重写,而抽象类中的方法只有abstract方法需要被重写。接口中所有的方法都不能实现,除非加default修饰,而抽象类中只有abstract方法不用实现。

面试问题三:什么是多态?

多态指的是一种思想,它能够降低类的调用者使用类的成本。就算不知道一个引用实例的是什么对象,只要在这个对象下有父类方法的重写,则实现一个静态方法能够调用子类当中的重写。而该方法的调用的重写能够调用多种子类的对象,并且有多种表现形式,就称为多态。