Skip to content

高级 ​

static 关键字 ​

假设有一个 Circle 类。

java
class Circle{
	private double radius;
	public Circle(double radius){
        this.radius=radius;
	}
	public double findArea(){
        return Math.PI*radius*radius;
    }
}

创建两个 Circle 对象:

Circle c1=new Circle(2.0); // c1.radius=2.0
Circle c2=new Circle(3.0); // c2.radius=3.0

Circle 类中的变量 radius 是一个实例变量(instance variable)。

c1 中的 radius 变化不会影响 c2 的 radius。

如果想让一个成员变量被类的所有实例所共享,就用 static 修饰即可,称为类变量(或类属性)!

TIP

  • 使用范围
    • 在 Java 类中,可用 static 修饰属性、方法、代码块、内部类
  • 被修饰后的成员具备的特点
    • 随着类的加载而加载
    • 优先于对象存在
    • 修饰的成员,被所有对象所共享
    • 访问权限允许时,可不创建对象,直接被类调用

类属性、类方法的设计思想 ​

当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象, 只有通过 new 关键字才会产出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。

我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份。

例如,所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

除了刚才说的类属性之外,在类中声明的实例方法,在类的外面必须要先创建对象,才能调用。 但是有些方法的调用者和当前类的对象无关,这样的方法通常被声明为类方法。

这里的类变量、类方法,只需要使用 static 修饰即可。所以也称为静态变量、静态方法。

语法格式 ​

[修饰符] class 类{
	[其他修饰符] static 数据类型 变量名;
}

静态变量的特点 ​

  • 静态变量的默认值规则和实例变量一样
  • 静态变量值被所有对象共享
  • 静态变量在本类中,可以在任意方法、代码块、构造器中直接使用
  • 如果权限修饰符允许,在其他类中可以通过 类名.静态变量 直接访问,也可以通过 对象.静态变量 的方式访问。 但是更推荐使用 类名.静态变量 的方式
  • 静态变量的 get/set 方法也是静态的,当局部变量与静态变量重名时,使用 类名.静态变量 进行区分

举例:

java
package javacode.oop3;

class Chinese1 {
    // 实例变量
    String name;
    int age;
    // 类变量
    static String nation; // 国籍

    public Chinese1() {
    }

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

public class Example1 {
    public static void main(String[] args) {
        Chinese1 c1 = new Chinese1("康师傅", 36);
        c1.nation = "中华人民共和国";

        Chinese1 c2 = new Chinese1("老干妈", 66);

        System.out.println(c1);
        System.out.println(c2);

        System.out.println(Chinese1.nation);
    }
}

TIP

如果以经典的 JDK6 内存为例,此时静态变量存储在方法区。

内存解析 ​

局部变量的值存储在栈。

静态变量的值被所有对象共享。静态变量的值存储在方法区。

实例变量的值属于每一个单独对象。实例变量的值存储在堆。

静态方法 ​

语法格式 ​

用 static 修饰的成员方法就是静态方法。

[修饰符] class 类{
	[其他修饰符] static 返回值类型 方法名(形参列表){
        方法体
    }
}

静态方法的特点 ​

  • 静态方法在本类的任意方法、代码块、构造器中都可以直接被调用
  • 只要权限修饰符允许,静态方法在其他类中可以通过 类名.静态方法 的方式调用。 也可以通过 对象.静态方法 的方式调用。推荐使用 类名.静态方法 的方式。
  • 在 static 方法内部只能访问类的 static 修饰的属性或方法,不能访问类的非 static 的结构
  • 静态方法可以被子类继承,但不能被子类重写
  • 静态方法的调用都只看编译时类型
  • 因为不需要实例就可以访问 static 方法,因此 static 方法内部不能有 this,也不能有 super。 如果有重名问题,使用 类名. 形式进行区别

容易做错的题 ​

如下程序执行会不会报错?

java
package javacode.oop3;

public class StaticTest {
    public static void main(String[] args) {
        Demo test = null;
        test.hello();
    }
}

class Demo{
    public static void hello(){
        System.out.println("hello!");
    }
}

答案

静态方法的使用不依靠对象,只看类型,在编译时就确定了。

单例(Singleton)设计模式 ​

设计模式概述 ​

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。 设计模式免去我们自己再思考和摸索。 它就像是经典的棋谱,不同的棋局,我们用不同的棋谱。

经典的设计模式共有 23 种。每个设计模式均是特定环境下特定问题的处理方法。

创建型模式结构性模式行为型模式
简单工厂模式外观模式模板方法模式
工厂方法模式外观模式观察者模式
抽象工厂模式适配器模式状态模式
创建者模式代理模式策略模式
原型模式装饰模式职责链模式
单例模式桥接模式命令模式
组合模式访问者模式
享元模式调停者模式
备忘录模式
迭代器模式
解释器模式

简单工厂模式并不是 23 中经典模式的一种,是其中工厂方法模式的简化版

何为单例模式 ​

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

实现思路 ​

如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为 private。 这样,就不能用 new 操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。 因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象, 静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。

单例模式的两种实现方式 ​

提前备好式 ​

java
package javacode.oop3;

public class HungrySingleOne {
    // 1 私有化构造器
    private HungrySingleOne() {
    }

    // 2 内部提供一个当前类的实例
    // 4 此实例也必须静态化
    private static HungrySingleOne single = new HungrySingleOne();

    // 3 提供公共的静态的方法,返回当前类的对象
    public static HungrySingleOne getInstance() {
        return single;
    }
}

要用才创建式 ​

java
package javacode.oop3;

public class PreviewSingleOne {
    // 私有化构造器
    private PreviewSingleOne() {
    }
    // 内部提供一个当前类的实例
    // 此实例也必须静态化
    private static PreviewSingleOne single;
    // 提供公共的静态的方法,返回当前类的对象
    public static PreviewSingleOne getInstance() {
        if(single == null) {
            single = new PreviewSingleOne();
        }
        return single;
    }
}

饿汉式 vs 懒汉式 ​

饿汉式:

  • 特点:立即加载,即在使用类的时候已经将对象创建完毕
  • 优点:实现起来简单;没有多线程安全问题
  • 缺点:当类被加载的时候,会初始化 static 的实例,静态变量被创建并分配内存空间。 从这以后,这个 static 的实例便一直占着这块内存,直到类被卸载时,静态变量被摧毁,才释放所占有的内存。 因此在某些特定条件下会耗费内存

懒汉式:

  • 特点:延迟加载,即在调用静态方法时实例才被创建
  • 优点:实现起来比较简单;当类被加载的时候,static 的实例未被创建并分配内存空间。 当静态方法第一次被调用时,初始化实例变量,并分配内存,因此在某些特定条件下会节约内存
  • 缺点:在多线程环境中,这种实现方法是完全错误的,线程不安全,根本不能保证单例的唯一性。但是可以再进行改进,让其变成线程安全的模式

单例模式的优点及应用场景 ​

由于单例模式只生成一个实例,所以减少了系统性能开销。当一个对象的产生需要比较多的资源时, 如读取配置、产生其他依赖对象时,则可以选择在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决这个问题。

应用场景:

  • Windows 的 Task Manager(任务管理器)就是很典型的单例模式
  • Windows 的 Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例
  • Application 也是单例的典型应用
  • 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,导致只能有一个实例去操作,否则内容不好追加
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源

理解 main 方法的语法 ​

由于 JVM 需要调用类的 main() 方法,所以该方法的访问权限必须是 public。 又因为 JVM 在执行 main() 方法时不必创建对象,所以该方法必须是 static 的, 该方法接收一个 String 类型的数组参数,该数组中保存执行 Java 命令时传递给所运行的类的参数。

又因为 main() 方法是静态的,我们不能直接访问该类中的非静态成员。

举个例子,如果下面的代码直接被 IDEA 运行,不设置参数,那么什么都不打印,因为没有任何参数。

java
package javacode.oop3;

public class CommandParams {
    public static void main(String[] args) {
        for (int i = 0; i < args.length; i++) {
            System.out.println("args[" + i + "] = " + args[i]);
        }
    }
}

但如果我们使用命令行带有参数地执行这个 .java 文件,那么就会打印我们加的参数。

shell
# 执行命令
javac CommandParams.java
java CommandParams "adsaf"

# 打印
args[0] = adsaf
shell
# 执行命令
javac CommandParams.java
java CommandParams --a=1213

# 打印
args[0] = --a=1213

如果想让 IDEA 直接运行 main 方法时也打印参数,可以点击 Edit Configrations 进行配置。

面试题 ​

下述程序是否可以正常编译、运行?

java
package javacode.oop3;

// 此处,OtherThing 类的文件名叫 Something.java
class OtherThing {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

答案

可以运行。因为类的修饰符是缺省状态。如果修饰符为 public,文件名与类名需要保持一致。

类的成员之一:代码块 ​

如果成员变量想要初始化的值不是一个硬编码的常量值, 而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值,该怎么办呢? 此时,可以考虑代码块(或叫初始化块)。

代码块(或初始化块)的作用:

  • 对 Java 类或对象进行初始化

代码块(又叫初始化块)的分类:

  • 一个类中代码块若有修饰符,则只能被 static 修饰,称为静态代码块(static block)
  • 没有使用 static 修饰的,为非静态代码块

静态代码块 ​

如果想要为静态变量初始化,可以直接在静态变量的声明后面直接赋值,也可以使用静态代码块。

静态代码块语法格式 ​

在代码块的前面加 static,就是静态代码块。

【修饰符】 class 类{
	static{
        静态代码块
    }
}

静态代码块的特点 ​

  1. 可以有输出语句
  2. 可以对类的属性、类的声明进行初始化操作
  3. 不可以对非静态的属性初始化,即不可以调用非静态的属性和方法
  4. 若有多个静态的代码块,那么按照从上到下的顺序依次执行
  5. 静态代码块的执行要先于非静态代码块
  6. 静态代码块随着类的加载而加载,且只执行一次
java
package javacode.oop3.staticblock;

public class People {
    static byte eyesNum;
    static byte noseNum;
    static byte handsNum;
    String name;
    People () {
        System.out.println("构造器执行了");
    }
    static {
        System.out.println("静态代码块加载");
        eyesNum = 2;
        noseNum = 1;
        handsNum = 2;
    }
    {
        name = "随机";
        System.out.println("非静态代码块加载");
    }
}

class PeopleTest {
    public static void main(String[] args) {
//        只执行静态代码块
//        System.out.println(People.eyesNum);

//        静态代码块、非静态代码块都执行
//        非静态代码块限于构造器内的代码执行
        new People();
    }
}

非静态代码块语法格式 ​

【修饰符】 class 类{
    {
        非静态代码块
    }
    【修饰符】 构造器名(){
    	// 实例初始化代码
    }
    【修饰符】 构造器名(参数列表){
        // 实例初始化代码
    }
}

非静态代码块的作用 ​

和构造器一样,也是用于实例变量的初始化等操作。

非静态代码块的意义 ​

如果多个重载的构造器有公共代码,并且这些代码都是先于构造器其他代码执行的,那么可以将这部分代码抽取到非静态代码块中,减少冗余代码。

非静态代码块的执行特点 ​

  1. 可以有输出语句
  2. 可以对类的属性、类的声明进行初始化操作
  3. 除了调用非静态的结构外,还可以调用静态的变量或方法
  4. 若有多个非静态的代码块,那么按照从上到下的顺序依次执行
  5. 每次创建对象的时候,都会执行一次。且先于构造器执行

练习 ​

声明 User 类 ​

  • 包含私有化属性:username(String 类型),password(String 类型),registrationTime(long 类型)
  • 包含 get/set 方法,其中 registrationTime 没有 set 方法
  • 包含无参构造
    • 输出“新用户注册”
    • registrationTime 赋值为当前系统时间
    • username 默认为当前系统时间值
    • password 默认为“123456”
  • 包含有参构造 (String username, String password)
    • 输出“新用户注册”
    • registrationTime 赋值为当前系统时间
    • username 和 password 由参数赋值
  • 包含 public String getInfo() 方法,返回:“用户名:xx,密码:xx,注册时间:xx”
java
package javacode.oop3.staticblock;

public class User {
    //    无参构造
    User() {
//        打印
        System.out.println("新用户注册");
//        赋值为当前系统时间
        registrationTime = System.currentTimeMillis();
        username = registrationTime + "";
        password = "123456";
    }

    //    有参构造
    User(String username, String password) {
        System.out.println("新用户注册");
//        赋值为当前系统时间
        registrationTime = System.currentTimeMillis();
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public long getRegistrationTime() {
        return registrationTime;
    }

    public String getInfo() {
        return "用户名:" + username +
                "密码:" + password +
                "注册时间:" + registrationTime;
    }

    private String username;
    private String password;


    private long registrationTime; // 注册时间

}

编写测试类 ​

如下。

java
package javacode.oop3.staticblock;

public class TestUser {
    public static void main(String[] args) {
        User u1 = new User();
        System.out.println(u1.getInfo());

        User u2 = new User("song", "8888");
        System.out.println(u2.getInfo());
    }
}

提取代码到非静态代码块 ​

java
package javacode.oop3.staticblock;

public class UserPlus {

    // ...
    //    非静态代码块
    {
        System.out.println("新用户注册");
//        赋值为当前系统时间
//        registrationTime = System.currentTimeMillis();
    }
    // ...

}

实例变量赋值顺序 ​

声明成员变量的默认初始化
↓
多个初始化代码块依次被执行(同级别下按先后顺序执行)
↓
构造器再对成员进行初始化
↓
通过 `对象.属性` 或 `对象.方法` 的方式,可以多次给属性赋值

分析加载顺序 1 ​

java
package javacode.oop3.staticblock;

class Root{
    static{
        System.out.println("Root 的静态初始化块");
    }
    {
        System.out.println("Root 的普通初始化块");
    }
    public Root(){
        System.out.println("Root 的无参数的构造器");
    }
}
class Mid extends Root{
    static{
        System.out.println("Mid 的静态初始化块");
    }
    {
        System.out.println("Mid 的普通初始化块");
    }
    public Mid(){
        System.out.println("Mid 的无参数的构造器");
    }
    public Mid(String msg){
        // 通过 this 调用同一类中重载的构造器
        this();
        System.out.println("Mid 的带参数构造器,其参数值:"
                + msg);
    }
}
class Leaf extends Mid{
    static{
        System.out.println("Leaf 的静态初始化块");
    }
    {
        System.out.println("Leaf 的普通初始化块");
    }
    public Leaf(){
        // 通过 super 调用父类中有一个字符串参数的构造器
        super("hello");
        System.out.println("Leaf 的构造器");
    }
}
public class LeafTest{
    public static void main(String[] args){
        new Leaf();
    }
}

第一步,按从父类到子类的顺序,执行各自的静态代码块。

第二步,按从父类到子类的顺序,执行各自的普通代码块 + 构造器。

TIP

  1. Root 的静态初始化块
  2. Mid 的静态初始化块
  3. Leaf 的静态初始化块
  • ~~
  1. Root 的普通初始化块
  2. Root 的无参数的构造器
  • ~~
  1. Mid 的普通初始化块
  2. Mid 的无参数的构造器
  3. Mid 的带参数构造器,其参数值:hello
  • ~~
  1. Leaf 的普通初始化块
  2. Leaf 的构造器

分析加载顺序 2 ​

如果入口 main 方法在子类中,那么先执行子类和父类的静态代码块,然后再执行 main 方法。

java
package javacode.oop3.staticblock;

class Father {
    static {
        System.out.println("11111111111");
    }

    {
        System.out.println("22222222222");
    }

    public Father() {
        System.out.println("33333333333");

    }

}

public class Son extends Father {
    static {
        System.out.println("44444444444");
    }

    {
        System.out.println("55555555555");
    }

    public Son() {
        System.out.println("66666666666");
    }


    public static void main(String[] args) {
        System.out.println("77777777777");
        System.out.println("************************");
        new Son();
        System.out.println("************************");

        new Son();
        System.out.println("************************");
        new Father();
    }

}

分析加载顺序 3 ​

静态属性先于静态方法加载。

java
package javacode.oop3.staticblock;

public class Test04 {
    public static void main(String[] args) {
        Zi zi = new Zi();
    }
}
class Fu{
    private static int i = getNum("(1)i");
    private int j = getNum("(2)j");
    static{
        print("(3)父类静态代码块");
    }
    {
        print("(4)父类非静态代码块,又称为构造代码块");
    }
    Fu(){
        print("(5)父类构造器");
    }
    public static void print(String str){
        System.out.println(str + "->" + i);
    }
    public static int getNum(String str){
        print(str);
        return ++i;
    }
}
class Zi extends Fu{
    private static int k = getNum("(6)k");
    private int h = getNum("(7)h");
    static{
        print("(8)子类静态代码块");
    }
    {
        print("(9)子类非静态代码块,又称为构造代码块");
    }
    Zi(){
        print("(10)子类构造器");
    }
    public static void print(String str){
        System.out.println(str + "->" + k);
    }
    public static int getNum(String str){
        print(str);
        return ++k;
    }
}

final 关键字 ​

final 英语意味最终的,不可更改的。

final 修饰类 ​

final 修饰类,表示这个类不能被继承,没有子类。提高安全性,提高程序的可读性。

例如:String 类、System 类、StringBuffer 类。

java
final class Eunuch{ // 太监类
	
}
class Son extends Eunuch{ // 错误
	
}

final 修饰方法 ​

表示这个方法不能被子类重写。

例如 Object 类中的 getClass()。

java
class Father{
	public final void method(){
		System.out.println("father");
	}
}
class Son extends Father{
	public void method(){ // 错误
		System.out.println("son");
	}
}

final 修饰变量 ​

final 修饰某个变量(成员变量或局部变量),一旦赋值,它的值就不能被修改,即常量,常量名建议使用大写字母。

例如 final double MY_PI = 3.14;。

注意

如果某个成员变量用 final 修饰后,没有 set 方法,那么必须初始化(可以显式赋值、或在初始化块赋值、实例变量还可以在构造器中赋值)。

这和 js 中用 const 来表示常量不同,const 修饰的变量在声明的时候必须同时初始化,否则就会报错。

js
const a; 

a = 1; 

修饰成员变量

java
public final class Test {
    public static int totalNumber = 5;
    public final int ID;

    public Test() {
        ID = ++totalNumber; // 可在构造器中给 final 修饰的“变量”赋值
    }
    public static void main(String[] args) {
        Test t = new Test();
        System.out.println(t.ID);
    }
}

修饰局部变量

java
public class TestFinal {
    public static void main(String[] args){
        final int MIN_SCORE ;
        MIN_SCORE = 0;
        final int MAX_SCORE = 100;
        MAX_SCORE = 200; // 非法
    }
}

错误演示

java
class A {
    private final String INFO = "atguigu"; // 声明常量

    public void print() {
        // The final field A.INFO cannot be  assigned
        // INFO = "尚硅谷"; 
    }
}

排错 ​

题 1

java
package javacode.oop3.final_test;

public class SomeThing {
    public int addOne(final int x) { // 用 final 修饰后,不能再赋值
//        return ++x; // error
         return x + 1;
    }
}

题 2

java
package javacode.oop3.final_test;

public class SomeThing2 {
    public static void main(String[] args) {
        Other o = new Other();
        new SomeThing2().addOne(o);
    }

    public void addOne(final Other o) {
        // o = new Other(); // error
    }
}

class Other {
    public int i;
}

abstract 关键字(抽象类与抽象方法) ​

由来 ​

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般、更通用。类的设计应该保证父类和子类能够共享特征。

有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。Java 用一个 abstract 修饰符表示。

换句话说,我们只在父类中规定方法的名字、方法的返回值类型,但是没有方法体,这样的类叫做抽象类,这样的方法叫做抽象方法。

Java 语法规定,包含抽象方法的类就是抽象类。但是抽象类不一定有抽象方法。

语法格式 ​

  • 抽象类:被 abstract 修饰的类
  • 抽象方法:被 abstract 修饰且没有方法体的方法

抽象类的语法格式:

[权限修饰符] abstract class 类名{
    
}
[权限修饰符] abstract class 类名 extends 父类{
    
}

抽象方法的语法格式:

[其他修饰符] abstract 返回值类型 方法名([形参列表]);

举例:

java
public abstract class Animal {
    public abstract void eat();
}

public class Cat extends Animal {
  public void eat (){
    System.out.println("小猫吃鱼和猫粮");
  }
}

public class CatTest {
  public static void main(String[] args) {
    // 创建子类对象
    Cat c = new Cat();

    // 调用 eat 方法
    c.eat();
  }
}

子类继承抽象父类,然后补充其没有方法体的抽象方法,这种操作算是一种重写,也经常被叫做“实现方法”。

使用说明 ​

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能用子类来继承父类,然后创建子类的对象

理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。 抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。

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

理解:子类的构造方法中,有默认的 super() 或手动的 super(实参列表),需要访问父类构造方法。

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

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

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

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

注意事项 ​

  • 不能用 abstract 修饰变量、代码块、构造器
  • 不能用 abstract 修饰私有方法、静态方法、final 的方法、final 的类

应用举例 1 ​

在交通运输公司系统中,Vehicle(交通工具)类需要定义两个方法分别计算运输工具的燃料效率和行驶距离。

卡车(Truck)和驳船(RiverBarge)的燃料效率和行驶距离的计算方法完全不同,但是它们计算返回的结果类型是相同的。 Vehicle 类不能提供计算方法,但继承它的子类可以。

交通工具抽象类。

java
package javacode.oop3.example1;

public abstract class Vehicle {
    // 这个方法用来计算燃料效率
    public abstract double calcFuelEfficiency();
    // 这个方法用来计算计算行驶距离
    public abstract double calcTripDistance();
}

继承交通工具的卡车类。

java
package javacode.oop3.example1;

public class Truck extends Vehicle {
    @Override
    public double calcFuelEfficiency() {
        // 卡车的燃烧效率为 20%
        double num = 10 / 100.0;
        num += 0.1;
        return num;
    }

    @Override
    public double calcTripDistance() {
        // 卡车的行驶举例为 501km
        double distance = 500;
        distance ++;
        return distance;
    }
}

继承交通工具的驳船类。

java
package javacode.oop3.example1;

public class RiverBarge extends Vehicle {
    @Override
    public double calcFuelEfficiency() {
        // 驳船的的燃烧效率为 30%
        double xiaolv = 20 / 100.0;
        xiaolv += 0.1;
        return xiaolv;
    }

    @Override
    public double calcTripDistance() {
        // 驳船的行驶举例为 30000km
        double distance = 30001.0;
        distance ++;
        return distance;
    }
}

应用举例 2(TemplateMethod 模板方法设计模式) ​

抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造, 但子类总体上会保留抽象类的行为方式。

为了解决的问题:

  • 当功能内部一部分实现是确定的,另一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现
  • 在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。 但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式

获取获取执行某段代码的时间 ​

需要继承的模板代码:

java
package javacode.oop3.template1;

public abstract class GetCodeTimeTemplate {
    public final void getTime() {
        long start = System.currentTimeMillis();
        code();
        long end = System.currentTimeMillis();
        System.out.println("执行 code 方法所消耗的时间是:" + (end - start) + " 毫秒");
    }

    public abstract void code();
}

某个类想用它写好的获取某段代码执行所消耗的时间的方法,那么就继承这个抽象类:

java
package javacode.oop3.template1;

public class Test extends GetCodeTimeTemplate{
    @Override
    public void code() {
        for (int i = 0; i < 100000; i++) {
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.getTime();
    }
}

流程固定 ​

抽象模板类:

java
package javacode.oop3.template2;

// 银行流程模板
public abstract class BankProcessTemplate {
    // 具体方法,取号
    public void takeNumber() {
        System.out.println("===========");
        System.out.println("首先取号排队");
        System.out.println("===========");
    }

    // 虚拟抽象方法,没有方法体
    // 取完号后,然后做什么交易、业务
    public abstract void transact();

    // 流程是固定的,但是具体的交易方法需要子类去实现
    public void process() {
        this.takeNumber();
        // 有点像一个钩子函数
        this.transact();
        this.evaluate();
    }

    // 具体方法,评分
    public void evaluate() {
        System.out.println("===========");
        System.out.println("最后反馈评分");
        System.out.println("===========");
    }
}

继承模板的取钱类:

java
package javacode.oop3.template2;

public class DrawMoney extends BankProcessTemplate {
    @Override
    public void transact() {
        System.out.println("取款 100 万!!!!");
    }
}

继承模板的存钱类:

java
package javacode.oop3.template2;

public class ManageMoney extends BankProcessTemplate{
    @Override
    public void transact() {
        System.out.println("理财,存银行一个亿,每天取利息。");
    }
}

程序入口起点:

java
package javacode.oop3.template2;

public class TemplateMain {
    public static void main(String[] args) {
        DrawMoney drawMoney = new DrawMoney();
        drawMoney.process();

        ManageMoney manageMoney = new ManageMoney();
        manageMoney.process();
    }
}

TIP

要想调用类里的实例方法,必须通过类 new 出来的实例。有些类我们只需要创建一个实例,目的只是为了调用它的实例方法。 有些类我们需要创建许多个实例。

模板方法设计模式是编程中经常用得到的模式。各个框架、类库中都有他的影子,比如:

  • 数据库访问的封装
  • Junit 单元测试
  • JavaWeb 的 Servlet 中关于 doGet/doPost 方法调用
  • Hibernate 中模板程序
  • Spring 中 JDBCTemlate、HibernateTemplate 等

思考与练习 ​

思考 ​

  1. 为什么抽象类不可以使用 final 关键字声明?

用 final 修饰后,类就不能被继承了。

  1. 一个抽象类中可以定义构造器吗?

可以。当 new 抽象类的子类时,会调用抽象类的构造器。

  1. 是否可以这样理解:抽象类就是比普通类多定义了抽象方法,除了不能直接进行类的实例化操作之外,并没有任何的不同?

可以。

interface 接口 ​

生活中大家每天都在用 USB 接口,那么 USB 接口与 Java 中的接口有什么相同点呢?

USB(Universal Serial Bus,通用串行总线)是 Intel 公司开发的总线架构, 使得在计算机上添加串行设备(鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3、手机、数码相机、移动硬盘等)非常容易。

其实,不管是电脑上的 USB 插口,还是其他设备上的 USB 插口都只是遵循了 USB 规范的一种具体设备而已。

只要设备遵循 USB 规范,那么就可以与电脑互联,并正常通信。至于这个设备、电脑是哪个厂家制造的,内部是如何实现的,我们都无需关心。

Java 的软件系统会有很多模块组成,那么各个模块之间也应该采用这种面向接口的低耦合,为系统提供更好的可扩展性和可维护性。

概述 ​

接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要...则必须能...”的思想。

继承是一个"是不是"的 is-a 关系,而接口实现则是 "能不能" 的 has-a 关系。

例如电脑都预留了可以插入 USB 设备的 USB 接口,USB 接口具备基本的数据传输的开启功能和关闭功能。 你能不能用 USB 进行连接,或是否具备 USB 通信功能,就看你能否遵循 USB 接口规范。

例如 Java 程序是否能够连接使用某种数据库产品,那么要看该数据库产品能否实现 Java 设计的 JDBC 规范。

接口的本质是契约、标准、规范,就像我们的法律一样。制定好后大家都要遵守。

定义格式 ​

接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成 .class 文件。 但一定要明确它并不是类,而是另外一种引用数据类型。

[修饰符] interface 接口名 {
    // 接口的成员列表:
    // 带 public、static、fianl 修饰符的常量,即公共静态常量
    // 带 public、abstract 修饰符的方法,即公共抽象方法
    
    // 带 public、default 修饰符的方法(JDK1.8 以上),公共默认方法
    // 带 public、static 修饰符的方法(JDK1.8 以上),公共静态方法
    // 私有方法(JDK1.9以上),私有方法
}

示例代码:

java
package javacode.oop3.interfacetest;

public class Example1 implements MyInterface, MyInterface2 {
    @Override
    public void sayHello() {

    }

    @Override
    public void howEat() {
        MyInterface.super.howEat();
    }

    @Override
    public void howRun() {

    }
}

interface MyInterface {
    // 公共静态常量
    public static final Number age = 28;

    // 公共抽象方法
    public abstract void sayHello();

    // 公共默认方法
    public default void howEat() {
        System.out.println("用筷子吃饭");
    }

    // 公共静态方法
    public static void getConst() {
        System.out.println("我的年龄是 " + MyInterface.age);
    }

    // 私有方法
    private static void logAge() {
        System.out.println("我的年龄是 " + MyInterface.age);
    }
}

interface MyInterface2 {
    public abstract void howRun();
}

接口的成员说明 ​

接口中没有构造器,没有初始化块,因为接口中没有成员变量需要动态初始化。

在 JDK8.0 之前,接口中只允许出现:

  1. 公共的静态的常量,其中 public static final 可以省略
  2. 公共的抽象的方法,其中 public abstract 可以省略

理解

接口是从多个相似类中抽象出来的规范,不需要提供具体实现。

在 JDK8.0 时,接口中开始允许声明默认方法和静态方法:

  1. 公共的默认的方法:其中 public 可以省略,但是 default 不能省略
  2. 公共的静态的方法:其中 public 可以省略,但是 static 不能省略

在 JDK9.0 时,接口又增加了:

  1. 私有方法

接口的使用规则 ​

implements 类实现接口 ​

不能用接口来创建对象,但是接口可以被类实现(有点类似于继承)。

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

【修饰符】 class 实现类  implements 接口{
	// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口{
    // 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

注意

  1. 如果接口的实现类是非抽象类,那么必须重写接口中所有抽象方法
  2. 默认方法可以选择保留,也可以重写。重写时,default 单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了
  3. 接口中的静态方法不能被继承也不能被重写

举例:

java
package javacode.oop3.interfacetest;

public class Example2 {
    public static void main(String args[]) {
        Computer.show(new Flash());
        Computer.show(new Print());

        Computer.show(new USB() {
            public void start() {
                System.out.println("移动硬盘开始运行");
            }

            public void stop() {
                System.out.println("移动硬盘停止运行");
            }
        });
    }
}

interface USB {        //
    public void start();

    public void stop();
}

class Computer {
    public static void show(USB usb) {
        usb.start();
        System.out.println("=========== USB 设备工作 ========");
        usb.stop();
    }
};

class Flash implements USB {
    @Override
    public void start() {    // 重写方法
        System.out.println("U 盘开始工作。");
    }

    @Override
    public void stop() {        // 重写方法
        System.out.println("U 盘停止工作。");
    }
};

class Print implements USB {
    public void start() {    // 重写方法
        System.out.println("打印机开始工作。");
    }

    public void stop() {        // 重写方法
        System.out.println("打印机停止工作。");
    }
};

接口的多实现 ​

之前学过,在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。 一个类只能继承一个父类,但是可以实现多个接口。

实现格式:

【修饰符】 class 实现类  implements 接口1,接口2,接口3。。。{
	// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{
    // 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

TIP

接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。

举例:

java
package javacode.oop3.duoimplement;

public interface A {
    void showA();
}
java
package javacode.oop3.duoimplement;

public interface B {
    void showB();
}
java
package javacode.oop3.duoimplement;

public class C implements A,B{
    @Override
    public void showA() {

    }

    @Override
    public void showB() {

    }
}

接口的多继承(extends) ​

类只能继承一个父类,但是接口可以继承一个或者多个接口。

接口的继承也使用 extends 关键字,子接口会继承父接口的方法。

定义父接口:

java
package javacode.oop3.duoextends;

public interface Chargeable {
    void charge();
    void in();
    void out();
}
java
package javacode.oop3.duoextends;

public interface USB {
    void start();
    void end();
}

定义子接口:

java
package javacode.oop3.duoextends;

public interface USB_Plus extends Chargeable, USB{
    double speed = 999;
    void reverse();
}

定义子接口的实现类,也是入口类:

java
package javacode.oop3.duoextends;

public class Main implements USB_Plus{
    @Override
    public void charge() {

    }

    @Override
    public void in() {

    }

    @Override
    public void out() {

    }

    @Override
    public void start() {

    }

    @Override
    public void end() {

    }

    @Override
    public void reverse() {

    }
}

TIP

所有父接口的抽象方法都有重写。

方法签名相同的抽象方法只需要实现一次。

接口与实现类对象构成多态引用 ​

实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。 通过接口类型的变量调用方法,最终执行的是你 new 的实现类对象实现的方法体。

接口的不同实现类:

java
package javacode.oop3.duoextends;

public class KeyBoard implements Chargeable {
    @Override
    public void charge() {

    }

    @Override
    public void in() {

    }

    @Override
    public void out() {

    }
}
java
package javacode.oop3.duoextends;

public class Mouse implements USB{
    @Override
    public void start() {

    }

    @Override
    public void end() {

    }
}

测试类:

java
package javacode.oop3.duoextends;

public class TestKM {
    public static void main(String[] args) {
        USB m = new Mouse();
        m.end();
        m.start();

        Chargeable keyBoard = new KeyBoard();
        keyBoard.charge();
        keyBoard.in();
        keyBoard.out();
    }
}

使用接口的静态成员 ​

接口不能直接创建对象,但是可以通过接口名直接调用接口的静态方法和静态常量。

java
package javacode.oop3.duoextends;

public class TestUsbPlus {
    public static void main(String[] args) {
        System.out.println("最高速度为 " + USB_Plus.speed);
    }
}

使用接口的非静态方法 ​

对于接口的静态方法,只能使用 接口名. 进行调用,不能通过实现类的对象进行调用。

对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用。

带有默认方法的接口:

java
package javacode.oop3.duoextends;

public interface TestMorenI {
    default void sayHello() {
        System.out.println("hello,很高兴见到你!");
    }
}

实现该接口的类;

java
package javacode.oop3.duoextends;

public class TestMoren implements TestMorenI {
    public static void main(String[] args) {
        TestMoren testMoren = new TestMoren();
        testMoren.sayHello();
    }
}

JDK8 中相关冲突问题 ​

默认方法冲突问题 ​

当一个类继承了一个父类,又实现了一个接口,并且接口中又默认方法的命名和父类的方法重名时,子类就近选择执行父类的成员方法。

父类:

java
package javacode.oop3.congtu;

// 父类
public class Father {
    public void date(){ // 约会
        System.out.println("父类中的 date 方法");
    }
}

接口:

java
package javacode.oop3.congtu;

// 接口
public interface Friend {
    public default void date() { // 约会
        System.out.println("吃喝玩乐");
    }
}

子类(入口类):

java
package javacode.oop3.congtu;

public class Son extends Father implements Friend {

    //    @Override
    //    public void date() {
    //        System.out.println("完全重写");
    //    }
    //    public void callFather() {
              // 调用父类的 date 方法
    //        super.date();
    //    }
    //    // 调用接口的默认 date 方法
    //    public void callFriend() {
    //        Friend.super.date();
    //    }
    public static void main(String[] args) {
        Son son = new Son();
        // 如果不重写 date 方法,那么调用的是父类 Father 中的 date 方法
        son.date();
    }
}

多个接口的默认方法命名冲突 ​

当一个类同时实现了多个接口,这些接口中包含方法签名相同的默认方法时,怎么办呢?

进行重写。

接口一:

java
package javacode.oop3.duointer;

public interface BoyFriend {
    default void date() {
        System.out.println("和男朋友约会");
    }
}

接口二:

java
package javacode.oop3.duointer;

public interface Friend {
    default void date() {
        System.out.println("普通朋友约会");
    }
}

实现类:

java
package javacode.oop3.duointer;

public class Girl implements BoyFriend, Friend {
//    需要重写两个接口中冲突的默认 date 方法,否则会飘红
    @Override
    public void date() {
//        调用 BoyFriend 的默认方法
//        BoyFriend.super.date();
//        调用 Friend 的默认方法
//        Friend.super.date();
    }

}

TIP

子接口重写默认方法时,default 关键字可以保留。

子类重写默认方法时,default 关键字不可以保留。

常量冲突问题 ​

当子类继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,并且该成员变量名在子类中仍然可见;

当子类同时实现多个接口,而多个接口存在相同同名常量。

此时在子类中想要引用父类或父接口的同名的常量或成员变量时,就会有冲突问题。所以需要明确调用的是哪一个常量。

母接口:

java
package javacode.oop3.finalcongtu;

public interface MotherInterface {
    int x = 123;
}

父接口:

java
package javacode.oop3.finalcongtu;

public interface SuperInterface {
    int x = 999;
    int y = 1000;
}

父类:

java
package javacode.oop3.finalcongtu;

public class SuperClass {
    int x = 1;
}

子类(入口类):

java
package javacode.oop3.finalcongtu;

public class SubClass extends SuperClass implements MotherInterface, SuperInterface{
    public static void main(String[] args) {
        // System.out.println("x = " + x); // 模糊不清
        // Reference to 'x' is ambiguous, both 'SuperClass.x' and 'SuperInterface.x' match
//    System.out.println("super.x = " + super.x); // 不行
        // 明确调用接口 SuperInterface 的静态常量 x
        System.out.println("SuperInterface.x = " + SuperInterface.x); // ok
        // 明确调用母类的 x
        System.out.println("MotherInterface.x = " + MotherInterface.x);
        System.out.println("y = " + y); // 没有重名问题,可以直接访问
    }
}

接口的总结与面试题 ​

总结 ​

  • 接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用
  • 声明接口用 interface 关键字,接口的成员的修饰符也有一些限制:
    • 可以是公共的静态常量
    • 可以是公共的抽象方法
    • 可以是公共的默认方法(JDK8.0 及以上)
    • 可以是公共的静态方法(JDK8.0 及以上)
    • 私有方法(JDK9.0 及以上)
  • 类可以实现接口,关键字是 implements,而且支持多实现。 如果实现类不是抽象类,就必须实现接口中所有的抽象方法。 如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后
  • 接口可以继承接口,关键字是 extends,而且支持多继承
  • 接口的默认方法可以选择重写或不重写。如果有冲突问题,另行处理。 子类重写父接口的默认方法,要去掉 default;子接口重写父接口的默认方法,不用去掉 default
  • 接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过 接口名.静态方法名 进行调用

面试题 ​

1、为什么接口中只能声明公共的静态的常量?

因为接口是标准规范,那么可能需要在规范中需要声明一些底线边界值。 当实现者在实现这些规范时,不能去随意修改和触碰这些底线,否则很有可能发生“危险”。

例如:USB1.0 规范中规定最大传输速率是 1.5Mbps,最大输出电流是 5V/500mA; USB3.0 规范中规定最大传输速率是 5Gbps(500MB/s),最大输出电流是 5V/900mA

2、为什么 JDK8.0 之后允许接口定义静态方法和默认方法呢?

因为它违反了接口作为一个抽象标准定义的概念。

  • 静态方法:因为之前的标准类库设计中,有很多 Collection/Colletions 或者 Path/Paths 这样成对的接口和类, 后面的类中都是静态方法,而这些静态方法都是为前面的接口服务的,那么这样设计一对 API, 不如把静态方法直接定义到接口中使用和维护更方便。

  • 默认方法:

    • 我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会导致原来使用这些接口的类出现问题, 因为它们没有重写我们新添加的抽象方法。 为了保持与旧版本代码的兼容性,Java 允许在接口中定义默认方法实现。 比如 Java8 中对 Collection、List、Comparator 等接口提供了丰富的默认方法
    • 如果我们接口的某个抽象方法,在很多实现类中的实现代码都是一样的,那么此时将这个抽象方法设计为默认方法更为合适。 这样实现类可以选择重写,也可以选择不重写。

3、为什么 JDK1.9 要允许接口定义私有方法呢?

因为有了默认方法和静态方法这样具有具体实现的方法,那么就可能出现多个方法由共同的代码可以抽取, 而这些共同的代码抽取出来的方法又只希望在接口内部使用,所以就增加了私有方法。

接口与抽象类之间的对比 ​

No.区别点抽象类接口
1定义可以包含抽象方法的类主要是抽象方法和全局常量的集合
2组成构造方法、抽象方法、普通方法、常量、变量常量、抽象方法、(jdk8.0 后有默认方法、静态方法)
3使用子类继承(extends)父类子类实现(implements)接口
4关系抽象类可以实现多个接口接口不能继承抽象类,但接口可以继承多个接口
5常见设计模式模板方法简单工厂、工厂方法、代理模式
6对象实现类的类型都可以被声明为抽象类或接口的类型,构成多态和抽象类一样
7局限抽象类只支持单继承接口支持多继承,也支持多实现
8实际作为一个模板作为一个标准或是表示一种能力
9选择如果抽象类和接口都可以使用的话,优先使用接口,摆脱抽象类的单继承的局限性-

在开发中,常看到一个类不是去继承一个已经实现好的类,而是要么继承抽象类,要么实现接口。

练习 ​

排错 1 ​

java
package javacode.oop3.interface_err;

public class Test1 {
}
interface A {
    int x = 0;
}
class B {
    int x = 1;
}
class C extends B implements A {
    public void pX() {
        // 这里的 x 指向模糊不清
//        System.out.println(x);
//        需要像下面这样明确指定
        // 打印父类 B 的 x
        System.out.println(super.x);;
        // 打印接口 A 的 x
        System.out.println(A.x);
    }
    public static void main(String[] args) {
        new C().pX();
    }
}

排错 2 ​

java
package javacode.oop3.interface_err;

public class Test2 {
}
interface Playable {
    void play();
}

interface Bounceable {
    void play();
}

interface Rollable extends Playable, Bounceable {
    // 接口的成员变量都带有 public static final 修饰符,只是省略没有写
    Ball ball = new Ball("PingPang");

}

class Ball implements Rollable {
    private String name;

    public String getName() {
        return name;
    }

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

    public void play() {
//        接口的成员变量带有 final 修饰符,不能被重新赋值
//        ball = new Ball("Football");
        System.out.println(ball.getName());
    }
}

练习 1 ​

先定义一个接口用来实现两个对象的比较:

java
interface CompareObject {
    // 若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小
    public int compareTo(Object o);
}

定义一个 Circle 类,声明 redius 属性,提供 getter 和 setter 方法。

定义一个 ComparableCircle 类,继承 Circle 类并且实现 CompareObject 接口。 在 ComparableCircle 类中给出接口中方法 compareTo 的实现体,用来比较两个圆的半径大小。

定义一个测试类 InterfaceTest,创建两个 ComparableCircle 对象,调用 compareTo 方法比较两个类的半径大小。

java
package javacode.oop3.interface_err;

public class Lianxi1 {
}

// #region CompareObject
interface CompareObject {
    // 若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小
    public int compareTo(Object o);
}
// #endregion CompareObject

class Circle {
    public double getRadius() {
        return radius;
    }

    public void setRadius(double redius) {
        this.radius = redius;
    }

    double radius;

}

class ComparabloeCircle extends Circle implements CompareObject {
    @Override
    public int compareTo(Object o) {
        double thisRadius = getRadius();
        double otherRadius = ((Circle) o).getRadius();
        double v = thisRadius - otherRadius;
        if (v > 0) return 1;
        if (v < 0) return -1;
        return 0;
    }
}

内部类(InnerClass) ​

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

为什么要声明内部类呢?

当一个事物 A 的内部,还有一个部分需要一个完整的结构 B 进行描述,而这个内部的完整的结构 B 又只为外部事物 A 提供服务, 不在其他地方单独使用,那么整个内部的完整结构 B 最好使用内部类。

遵循高内聚、低耦合的面向对象开发原则。

根据内部类声明的位置(如同变量的分类),我们可以这样分:

  • 内部类
    • 成员内部类
      • 静态成员内部类
      • 非静态成员内部类
    • 局部内部类
      • 非匿名局部内部类
      • 匿名局部内部类

成员内部类 ​

语法格式及概述 ​

成员内部类的语法格式:

[修饰符] class 外部类{
    [其他修饰符] [static] class 内部类{
    }
}

成员内部类的使用特征可以有两种角色。

一,成员内部类作为类的成员的角色:

  • 和外部类不同,Inner class 还可以用 private 或 protected 进行修饰
  • 可以调用外部类的结构(静态内部类不能使用外部类的实例成员)
  • 内部类可以声明为静态的,但这样就不能使用外层类的实例变量

二,成员内部类作为类的角色:

  • 内部类可以在自己内部定义属性、方法、构造器等结构
  • 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
  • 可以声明为 abstract 类,因此可以被其它的内部类继承
  • 可以声明为 final 的,表示不能被继承
  • 编译以后生成 OuterClass$InnerClass.class 字节码文件(也适用于局部内部类)

注意点:

  • 外部类访问成员内部类的成员,需要通过内部类.成员或内部类对象.成员的方式
  • 成员内部类可以直接使用外部类的所有成员,包括私有的数据
  • 当想要在外部类的静态成员部分使用内部类时,可以考虑将内部类声明为静态的

创建成员内部类对象 ​

如果是静态内部类,那么这样实例化:

外部类名.静态内部类名 变量 = 外部类名.静态内部类名();
变量.非静态方法();

如果是实例内部类,那么这样实例化:

外部类名 变量1 = new 外部类();
外部类名.非静态内部类名 变量2 = 变量1.new 非静态内部类名();
变量2.非静态方法();

举例 ​

java
package javacode.oop3.inner_class;

public class TestMemberInnerClass {
    public static void main(String[] args) {
        // 创建静态内部类实例,并调用方法
        // 直接通过 `new 类。静态类名 ()` 创建静态内部类实例
        Outer.StaticInner inner = new Outer.StaticInner();
        inner.inFun();

        // 调用静态内部类静态方法
        Outer.StaticInner.inMethod();

        System.out.println("*****************************");

        // 创建非静态内部类实例(方式 1),并调用方法
        // 因为不是静态的,所以不能通过类直接访问到内部类,
        // 而应该通过实例来 new 对象
        Outer outer = new Outer();
        Outer.NoStaticInner inner1 = outer.new NoStaticInner();
        inner1.inFun();

        // 创建非静态内部类实例(方式 2)
        Outer.NoStaticInner inner2 = outer.getNoStaticInner();
        inner1.inFun();

        // 不能通过这种方式创建非静态内部类,会报 Cannot resolve symbol 'NoStaticInner' 错误
//        随便获取一个不存在的属性,也会报 Cannot resolve symbol 'adb' 错误
//        outer.adb
//        说明非静态内部类其实并不是类的一个成员
//        new outer.NoStaticInner();
    }
}

class Outer {
    private static String a = "外部类的静态 a";
    private static String b  = "外部类的静态 b";
    private String c = "外部类对象的非静态 c";
    private String d = "外部类对象的非静态 d";

    // 静态内部类
    static class StaticInner{
        private static String a ="静态内部类的静态 a";
        private String c = "静态内部类对象的非静态 c";
        public static void inMethod(){
            System.out.println("Inner.a = " + a);
            System.out.println("Outer.a = " + Outer.a);
            System.out.println("b = " + b);
        }
        public void inFun(){
            System.out.println("Inner.inFun");
            System.out.println("Outer.a = " + Outer.a);
            System.out.println("Inner.a = " + a);
            System.out.println("b = " + b);
            System.out.println("c = " + c);
//            System.out.println("d = " + d); // 不能访问外部类的非静态成员
        }
    }
    // 非静态内部类
    class NoStaticInner{
        private String a = "非静态内部类对象的非静态 a";
        private String c = "非静态内部类对象的非静态 c";

        public void inFun(){
            System.out.println("NoStaticInner.inFun");
            System.out.println("Outer.a = " + Outer.a);
            System.out.println("a = " + a);
            System.out.println("b = " + b);
            System.out.println("Outer.c = " + Outer.this.c);
            System.out.println("c = " + c);
            System.out.println("d = " + d);
        }
    }
    public NoStaticInner getNoStaticInner(){
        return new NoStaticInner();
    }
}

局部内部类 ​

非匿名局部内部类 ​

语法格式:

[修饰符] class 外部类{
    [修饰符] 返回值类型  方法名(形参列表){
            [final/abstract] class 内部类{
    	}
    }    
}

说明:

  • 非匿名局部内部类编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名、$ 符号、编号。这里有编号是因为同一个外部类中,不同的方法中存在相同名称的局部内部类
  • 和成员内部类不同的是,它前面不能有权限修饰符等符号
  • 局部内部类如同局部变量一样,有作用域
  • 局部内部类中是否能访问外部类的非静态的成员,取决于所在的方法是静态的还是非静态的

举例:

java
package javacode.oop3.part_inner;

public class TestPartInner {
    public static void main(String[] args) {

    }
}

class Outer {
    public static void outMethod(){
        System.out.println("Outer.outMethod");
        final String c = "局部变量 c";
        class Inner {
            public void inMethod(){
                System.out.println("Inner.inMethod");
                System.out.println(c);
            }
        }

        Inner in = new Inner();
        in.inMethod();
    }
    public void outTest(){
        class Inner{
            public void inMethod1(){
                System.out.println("Inner.inMethod1");
            }
        }

        Inner in = new Inner();
        in.inMethod1();
    }

    public static Runner getRunner(){
        class LocalRunner implements Runner{
            @Override
            public void run() {
                System.out.println("LocalRunner.run");
            }
        }
        return new LocalRunner();
    }
}

interface Runner{
    void run();
}

匿名内部类 ​

考虑到有时候内部的子类或实现类是一次性的,那么我们“费尽心机”的给它取名字,就显得多余。 那么我们完全可以使用匿名内部类的方式来实现,避免给类命名的问题。

举例 1,使用匿名内部类的对象直接调用方法:

java
package javacode.oop3.annymous_inner;

public class Test1 {
    public static void main(String[] args) {
        new A() {
            @Override
            public void a() {
                System.out.println("hello");
            }
        }.a();
    }
}

interface A{
    void a();
}

举例 2,通过父类或父接口的变量多态引用匿名内部类的对象:

java
package javacode.oop3.annymous_inner;


public class Test2 {
    public static void main(String[] args) {
        B b = new B() {
            @Override
            public void a() {

            }
        };
        b.a();
    }
}

interface B {
    void a();
}

举例 3,匿名内部类的对象作为实参

java
package javacode.oop3.annymous_inner;

public class Test3 {
    public static void test(C a){
        a.method();
    }
    public static void main(String[] args) {
        test(new C(){

            @Override
            public void method() {
                System.out.println("aaaa");
            }
        });
    }
}

interface C {
    void method();
}

练习 ​

1 ​

判断输出结果为何?

java
package javacode.oop3.inner_class_test;

public class Test1 {
    public Test1() {
        Inner s1 = new Inner();
        s1.a = 10;
        Inner s2 = new Inner();
        s2.a = 20;
        Test1.Inner s3 = new Test1.Inner();
        System.out.println(s3.a);
    }
    class Inner {
        public int a = 5;
    }
    public static void main(String[] args) {
        Test1 t = new Test1();
        Inner r = t.new Inner();
        System.out.println(r.a); // 5
    }
}

2 ​

编写一个匿名内部类,它继承 Object,并在匿名内部类中,声明一个方法 public void test() 打印 hello world。

请编写代码调用这个方法。

java
package javacode.oop3.inner_class_test;

public class Test2 {
    public static void main(String[] args) {
        new Object(){
            public void test(){
                System.out.println("hello world");
            }
        }.test();
    }
}

枚举类 ​

概述 ​

枚举类型本质上也是一种类,只不过是这个类的对象是有限的、固定的几个,不能让用户随意创建。

枚举类的例子举不胜举:

  • 星期:Monday(星期一)......Sunday(星期天)
  • 性别:Man(男)、Woman(女)
  • 月份:January......December
  • 季节:Spring......Winter
  • 三原色:red、green、blue
  • 支付方式:Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银行卡)、CreditCard(信用卡)
  • 就职状态:Busy、Free、Vocation、Dimission
  • 订单状态:Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配货)、Delivered(已发货)、Checked(已确认收货)、Return(退货)、Exchange(换货)、Cancel(取消)
  • 线程状态:创建、就绪、运行、阻塞、死亡

若枚举只有一个对象,则可以作为一种单例模式的实现方式。

在 JDK5.0 之前,需要程序员自定义枚举类型。 在 JDK5.0 之后,Java 支持 enum 关键字来快速定义枚举类型。

JDK5.0 之前定义枚举类 ​

在 JDK5.0 之前如何声明枚举类呢?

  • 需要私有化类的构造器,保证不能在类的外部创建其对象
  • 在类的内部创建枚举类的实例,修饰符为 public static final,以对外暴露这些常量对象
  • 对象如果有实例变量,建议修饰符为 private final,并在构造器中初始化

示例代码:

java
package javacode.oop3.enum_test;

public class Example1 {
    public static void main(String[] args) {
        System.out.println(Season.AUTUMN);
        System.out.println(Season.AUTUMN.toString());
    }
}

class Season {
    private final String SEASON_NAME; // 季节的名称
    private final String SEASON_DESC; // 季节描述
    private Season(String SEASON_NAME, String SEASON_DESC) {
        this.SEASON_NAME = SEASON_NAME;
        this.SEASON_DESC = SEASON_DESC;
    }
    public static final Season SPRING = new Season("春天", "春暖花开");
    public static final Season SUMMER = new Season("夏天", "夏日炎炎");
    public static final Season AUTUMN = new Season("秋天", "秋高气爽");
    public static final Season WINTER = new Season("冬天", "白雪皑皑");

    @Override
    public String toString() {
        return this.SEASON_NAME + this.SEASON_DESC;
    }
}

JDK5.0 之后定义枚举类 ​

enum 关键字声明枚举 ​

【修饰符】 enum 枚举类名{
    常量对象列表
}

【修饰符】 enum 枚举类名{
    常量对象列表;
    
    对象的实例变量列表;
}

举例:

java
package javacode.oop3.enum_test;

public enum Week {
    ONE,
    TWO,
    THREE,
    FOUR,
    FIVE,
    SIX,
    SEVEN
}

class TesT {
    public static void main(String[] args) {
        System.out.println(Week.SIX);
        // true
        // Week.SIX 其实是一个对象
        System.out.println(Week.SIX instanceof Object);

    }
}

enum 定义的要求和特点 ​

  • 枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写
  • 列出的实例系统会自动添加 public static final 修饰
  • 如果常量对象列表后面没有其他代码,那么冒号 ; 可以省略,否则不可以省略 ;
  • 编译器给枚举类默认提供的是 private 的无参构造,如果枚举类刚好需要的也是无参构造,就不需要声明,写常量对象列表时也不用加参数
  • 枚举类可以用有参构造器,但是需要我们自己定义。有参构造器的 private 修饰符可以省略,调用有参构造器的方法就是在常量对象名后面加 (实参列表) 就可以了
  • 枚举类默认继承的是 java.lang.Enum 类,因此不能再继承其他的类型
  • JDK5.0 之后 switch 支持枚举类型,case 后面可以写枚举常量名,无需添加枚举类作为限定

举例 1:

java
package javacode.oop3.enum_test;

public enum SeasonEnum {
    SPRING("春天", "春风又绿江南岸"),
    SUMMER("夏天", "映日荷花别样红"),
    AUTUMN("秋天", "秋水共长天一色"),
    WINTER("冬天", "窗舍西岭千秋雪");


    private SeasonEnum(String seaonName, String seasonDesc) {
        this.seaonName = seaonName;
        this.seasonDesc = seasonDesc;
    }
    private SeasonEnum() {

    }
    private String seaonName = "未知";
    private String seasonDesc = "未知";
}

举例 2:

java
package javacode.oop3.enum_test;

import java.util.StringJoiner;

public enum Week2 {
    MONADY("星期一"),
    TUESDAY("星期二"),
    WEDNESDAY("星期三"),
    THURSDAY("星期四"),
    FRIDAY("星期五"),
    SATURDAY("星期六"),
    SUNDY("星期日");

    private final String description;
    private Week2(String description) {
        this.description = description;
    }

//    @Override
//    public String toString() {
//        return description;
//    }
}

举例 3,switch 与 enum 的结合:

java
package javacode.oop3.enum_test;

public class TestWeek {
    public static void main(String[] args) {
        Week2 week = Week2.MONADY;
        System.out.println(week);
        switch (week) {
            case MONADY:
                System.out.println("怀念周末,困意很浓");break;
            case TUESDAY:
                System.out.println("进入学习状态");break;
            case WEDNESDAY:
                System.out.println("死撑");break;
            case THURSDAY:
                System.out.println("小放松");break;
            case FRIDAY:
                System.out.println("又信心满满");break;
            case SATURDAY:
                System.out.println("玩");
            case SUNDY:
                System.out.println("继续玩");

        }
    }
}

< 开发中,当需要定义一组常量时,强烈建议使用枚举类。

enum 中的常用方法 ​

String toString(): 默认返回的是常量名(对象名),可以继续手动重写该方法!
    
static 枚举类型[] values(): 返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值,是一个静态方法。
    
static 枚举类型 valueOf(String name): 可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。
    
String name(): 得到当前枚举常量的名称。建议优先使用 toString()。
    
int ordinal(): 返回当前枚举常量的次序号,默认从 0 开始。

实现接口的枚举类 ​

和普通 Java 类一样,枚举类可以实现一个或多个接口。

语法:

// 1、枚举类可以像普通的类一样,实现接口,并且可以多个,但要求必须实现里面所有的抽象方法!
enum A implements 接口1,接口2{
	// 抽象方法的实现
}

// 2、枚举类的常量可以继续重写抽象方法
enum A implements 接口1,接口2{
    常量名1(参数){
        // 抽象方法的实现或重写
    },
    常量名2(参数){
        // 抽象方法的实现或重写
    },
    //...
}

举例:

java
package javacode.oop3.enum_test;

import java.sql.SQLOutput;

interface Info{
    void show();
}

public enum Seasion1 implements Info{
    // 1. 创建枚举类中的对象,声明在 enum 枚举类的首位
    SPRING("春天","春暖花开"){
        public void show(){
            System.out.println("春天在哪里?");
        }
    },
    SUMMER("夏天","夏日炎炎"){
        public void show(){
            System.out.println("宁静的夏天");
        }
    },
    AUTUMN("秋天","秋高气爽"){
        public void show(){
            System.out.println("秋天是用来分手的季节");
        }
    },
    WINTER("冬天","白雪皑皑"){

    };

    // 2. 声明每个对象拥有的属性:private final 修饰
    private final String SEASON_NAME;
    private final String SEASON_DESC;

    // 3. 私有化类的构造器
    private Seasion1(String seasonName,String seasonDesc){
        this.SEASON_NAME = seasonName;
        this.SEASON_DESC = seasonDesc;
    }

    public String getSEASON_NAME() {
        return SEASON_NAME;
    }

    public String getSEASON_DESC() {
        return SEASON_DESC;
    }

    @Override
    public void show() {
        System.out.println("统一实现方法");
    }
}

注解 Annotation ​

什么是注解 ​

注解(Annotation)是从 JDK5.0 开始引入,以 @注解名 的形式在代码中存在。例如:

@Override
@Deprecated
@SuppressWarnings(value=”unchecked”)

Annotation 可以像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明。 还可以添加一些参数值,这些信息被保存在 Annotation 的 name=value 对中。

注解可以在类编译、运行时进行加载,体现不同的功能。

注解与注释 ​

注解也可以看做是一种注释,通过使用 Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。 但是,注解,不同于单行注释和多行注释。

  • 单行注释和多行注释是给程序员看的。
  • 注解是可以被编译器或其他程序读取。程序还可以根据注解的不同,做出相应的处理。

注解的重要性 ​

在 JavaSE 中,注解的使用目的比较简单,一般用来标记过时的功能,忽略警告等。 在 JavaEE/Android 中注解占据了更重要的角色。

例如用注解来配置应用程序的任何切面,代替 JavaEE 旧版中所遗留的繁冗代码和 XML 配置等。

未来的开发模式都是基于注解的,JPA 是基于注解的,Spring2.5 以上都是基于注解的, Hibernate3.x 以后也是基于注解的,Struts2 有一部分也是基于注解的了。 注解是一种趋势,一定程度上可以说 框架 = 注解 + 反射 + 设计模式。

常见的 Annotation 作用 ​

示例 1 ​

生成文档相关的注解。

java
package javacode.oop3.annotation;

/**
 * @author lukecheng
 * @version 1.0.0
 * // `@see` 以为参考
 * @see javacode.oop3.annymous_inner.Test1
 */
public class Example1 {
    /**
     * @param args 命令行参数
     */
    public static void main(String[] args) {

    }
    /**
     * 求圆面积的方法
     * @param radius double 半径值
     * @return double 圆的面积
     */
    public static double getArea(double radius){
        return Math.PI * radius * radius;
    }
}

示例 2 ​

在编译时进行格式检查。这三个是 JDK 内置的三个基本注解。

  • @Override:限定重写父类方法,该注解只能用于方法
  • @Deprecated:用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
  • @SuppressWarnings:抑制编译器警告
java
package javacode.oop3.annotation;

public class Example2 {
    public static void main(String[] args) {
        @SuppressWarnings("unused")
        int aasdfas = 10;
    }
    @Deprecated
    public void sayHi() {
        System.out.println("hello");
    }
    @Override
    public String toString() {
        return "Example2";
    }
}

示例 3 ​

跟踪代码依赖性,实现替代配置文件功能。

Servlet3.0 提供了注解 annotation,使得不再需要在 web.xml 文件中进行 Servlet 的部署。

java
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    
    protected void doGet(HttpServletRequest request, HttpServletResponse response) { }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        doGet(request, response);
	}  
}
xml
 <servlet>
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>com.servlet.LoginServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/login</url-pattern>
  </servlet-mapping>

Spring 框架中关于“事务”的管理:

@Transactional(propagation=Propagation.REQUIRES_NEW,isolation=Isolation.READ_COMMITTED,readOnly=false,timeout=3)
public void buyBook(String username, String isbn) {
	// 1.查询书的单价
    int price = bookShopDao.findBookPriceByIsbn(isbn);
    // 2. 更新库存
    bookShopDao.updateBookStock(isbn);	
    // 3. 更新用户的余额
    bookShopDao.updateUserAccount(username, price);
}
xml
<!-- 配置事务属性 -->
<tx:advice transaction-manager="dataSourceTransactionManager" id="txAdvice">
       <tx:attributes>
       <!-- 配置每个方法使用的事务属性 -->
       <tx:method name="buyBook" propagation="REQUIRES_NEW" 
	 isolation="READ_COMMITTED"  read-only="false"  timeout="3" />
       </tx:attributes>
</tx:advice>

三个最基本的注解 ​

@Override ​

  • 用于检测被标记的方法为有效的重写方法,如果不是,则报编译错误
  • 只能标记在方法上
  • 它会被编译器程序读取

@Deprecated ​

  • 用于表示被标记的数据已经过时,不推荐使用
  • 可以用于修饰属性、方法、构造、类、包、局部变量、参数
  • 它会被编译器程序读取

@SuppressWarnings ​

  • 抑制编译警告。当我们不希望看到警告信息的时候,可以使用 SuppressWarnings 注解来抑制警告信息
  • 可以用于修饰类、属性、方法、构造、局部变量、参数
  • 它会被编译器程序读取

可以指定的警告类型有:

  • all,抑制所有警告
  • unchecked,抑制与未检查的作业相关的警告
  • unused,抑制与未用的程式码及停用的程式码相关的警告
  • deprecation,抑制与淘汰的相关警告
  • nls,抑制与非 nls 字串文字相关的警告
  • null,抑制与空值分析相关的警告
  • rawtypes,抑制与使用 raw 类型相关的警告
  • static-access,抑制与静态存取不正确相关的警告
  • static-method,抑制与可能宣告为 static 的方法相关的警告
  • super,抑制与置换方法相关但不含 super 呼叫的警告

元注解 ​

JDK1.5 在 java.lang.annotation 包定义了 4 个标准的 meta-annotation 类型, 它们被用来提供对其它 annotation 类型作说明。

(1)@Target: 用于描述注解的使用范围

  • 可以通过枚举类型 ElementType 的 10 个常量对象来指定
  • TYPE,METHOD,CONSTRUCTOR,PACKAGE.....

(2)@Retention: 用于描述注解的生命周期

  • 可以通过枚举类型 RetentionPolicy 的 3 个常量对象来指定
  • SOURCE(源代码)、CLASS(字节码)、RUNTIME(运行时)
  • 唯有 RUNTIME 阶段才能被反射读取到。

(3)@Documented 表明这个注解应该被 javadoc 工具记录

(4)@Inherited: 允许子类继承父类中的注解

示例代码:

java
package javacode.oop3.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;

public class MetaAnnotation {

}

// Override 注解
@Target(METHOD)
@Retention(RetentionPolicy.SOURCE)
@interface Override {
}

// SuppressWarnings 注解
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
@interface SuppressWarnings {
    String[] value();
}

// Deprecated 注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
@interface Deprecated {
}

自定义注解的使用 ​

一个完整的注解应该包含三个部分:

  1. 声明
  2. 使用
  3. 读取

声明自定义注解 ​

格式:

【元注解】
【修饰符】 @interface 注解名{
    【成员列表】
}
  • 自定义注解可以通过四个元注解 @Retention、@Target、@Inherited、@Documented, 分别说明它的声明周期,使用位置,是否被继承,是否被生成到 API 文档中
  • Annotation 的成员在 Annotation 定义中以无参数有返回值的抽象方法的形式来声明,我们又称为配置参数。 返回值类型只能是八种基本数据类型、String 类型、Class 类型、enum 类型、Annotation 类型中的一种
  • 可以使用 default 关键字为抽象方法指定默认返回值
  • 如果定义的注解含有抽象方法,除非它有默认值,否则使用时必须指定返回值。 格式是“方法名 = 返回值”,如果只有一个抽象方法需要赋值,且方法名为 value,可以省略“value=”,所以如果注解只有一个抽象方法成员,建议使用方法名 value
java
package javacode.oop3.annotation;

import java.lang.annotation.*;

public class StateAnnotation {
}

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Table {
    String value();
}

@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Column {
    String columnName();
    String columnType();
}

使用自定义注解 ​

java
package javacode.oop3.annotation;

@Table("t_su")
public class UseAnnotation {
    @Column(columnName = "sid",columnType = "int")
    private int id;
    @Column(columnName = "sname",columnType = "varchar(20)")
    private String name;

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

读取和处理自定义注解 ​

自定义注解必须配上注解的信息处理流程才有意义。

我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是 RetentionPolicy.RUNTIME。

JUnit 单元测试 ​

测试分类 ​

  • 黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值
  • 白盒测试:需要写代码的。关注程序具体的执行流程

JUnit 单元测试介绍 ​

JUnit 是由 Erich Gamma 和 Kent Beck 编写的一个测试框架(regression testing framework), 供 Java 开发人员编写单元测试之用。

JUnit 测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。

要使用 JUnit,必须在项目的编译路径中引入 JUnit 的库,即相关的 .class 文件组成的 jar 包。 jar 就是一个压缩包,压缩包都是开发好的第三方(Oracle 公司第一方,我们自己第二方,其他都是第三方)工具类,都是以 class 文件形式存在的。

引入本地 JUnit.jar ​

首先在项目中 File-Project Structure 中操作,添加 Libraries 库。

选择 jar 包文件。

选择要在哪些 module 中应用 JUnit 库。

再然后检查是否应用成功。

注意 Scope 那一列选择 Compile,否则编译时无法使用 JUnit。

下次如果有新的模块要使用该 libs 库,这样操作即可:

选择第二个 Library。

点击选中的库。

然后还需要把 scope 那一列的 Test 字段改为 Compile。

编写和运行 @Test 单元测试方法 ​

JUnit4 版本,要求 @Test 标记的方法必须满足如下要求:

  • 所在的类必须是 public 的,非抽象的,包含唯一的无参构造器
  • @Test 标记的方法本身必须是 public,非抽象的,非静态的,void 无返回值,方法无参数的
java
package javacode.oop3.test_junit;

import org.junit.Test;

public class TestJUnit {
    @Test
    public void test01(){
        System.out.println("TestJUnit.test01");
    }

    @Test
    public void test02(){
        System.out.println("TestJUnit.test02");
    }

    @Test
    public void test03(){
        System.out.println("TestJUnit.test03");
    }
}

设置执行 JUnit 用例时支持控制台输入 ​

默认情况下,在单元测试方法中使用 Scanner 时,并不能实现控制台数据的输入。需要做一些设置。

在 idea64.exe.vmoptions 配置文件中加入下面一行设置,然后重启 idea 后生效。

-Deditable.java.test.console=true

配置文件位置:

添加完成之后,重启 IDEA 即可。

如果上述位置设置不成功,需要继续修改。

修改位置 1:IDEA 安装目录的 bin 目录(例如:D:\develop_tools\IDEA\IntelliJ IDEA 2022.1.2\bin)下的 idea64.exe.vmoptions 文件。

修改位置 2:C 盘的用户目录 C:\Users\用户名\AppData\Roaming\JetBrains\IntelliJIdea2022.1 下的 idea64.exe.vmoptions 文件。

定义 test 测试方法模板 ​

选中自定义的模板组,点击”+”(Live Template)来定义模板。

包装类 ​

为什么需要包装类 ​

Java 提供了两个类型系统,基本数据类型与引用数据类型。但是有些参数要求必须是引用数据类型,这该怎么办呢?

// 情况 1:方法形参
Object 类的 equals(Object obj)

// 情况 2:方法形参
ArrayList 类的 add(Object obj)

// 没有如下的方法:
add(int number)
add(double d)
add(boolean b)

// ? 泛型
Set<T>
List<T>
Cllection<T>
Map<K,V>

有哪些包装类 ​

Java 针对八种基本数据类型定义了相应的引用类型:包装类(封装类)。有了类的特点,就可以调用类中的方法,Java 才是真正的面向对象。

基本数据类型包装类父类
byteByteNumber
shortShortNumber
intIntegerNumber
longLongNumber
floatFloatNumber
doubleDoubleNumber
booleanBoolean\
charCharacter\

封装以后的内存结构对比:

public static void main(String[] args){
	int num = 520;
	Integer obj = new Integer(520);
}

自定义包装类 ​

public class MyInteger {
    int value;

    public MyInteger() {
    }

    public MyInteger(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

包装类与基本数据类型间的转换 ​

装箱 ​

装箱:把基本数据类型转为包装类对象。

转为包装类的对象,是为了使用专门为对象设计的 API 和特性。

java
package javacode.oop3.baozhuanglei;

public class Test1 {
    public static void main(String[] args) {
        /**
         * new Integer(123) 被标记为 deprecated,
         * 现在推荐使用 Integer 类的工厂函数方法来生成一个 int 类型数据的包装类
         */
        /**
         * Constructs a newly allocated {@code Integer} object that
         * represents the specified {@code int} value.
         *
         * @param   value   the value to be represented by the
         *                  {@code Integer} object.
         *
         * @deprecated
         * It is rarely appropriate to use this constructor. The static factory
         * {@link #valueOf(int)} is generally a better choice, as it is
         * likely to yield significantly better space and time performance.
         */
        Integer integer = new Integer(4);
        // 推荐
        Integer integer1 = Integer.valueOf(999);

        Float aFloat = Float.valueOf(10.1000f);

        Double aDouble = Double.valueOf(10.98);

        Float.valueOf("asdf"); // NumberFormatException


//        报错,提示 'Number' is abstract; cannot be instantiated
//        Number number = new Number(4);
    }
}

拆箱 ​

拆箱:把包装类对象拆为基本数据类型。

转为基本数据类型,一般是因为需要运算,Java 中的大多数运算符是为基本数据类型设计的,比如比较、算术等。

java
package javacode.oop3.baozhuanglei;

public class Test2 {
    public static void main(String[] args) {
        Integer obj = Integer.valueOf(4);
        int num1 = obj.intValue();
    }
}

由于我们经常要做基本类型与包装类之间的转换,从 JDK5.0 开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:

java
package javacode.oop3.baozhuanglei;

public class Test3 {
    public static void main(String[] args) {
        Integer i = 4; // 自动装箱。相当于 Integer i = Integer.valueOf(4);
        i = i + 5; // 等号右边:将 i 对象转成基本数值(自动拆箱),相当于 i.intValue() + 5;
        // 加法运算完成后,再次装箱,把基本数值转成对象

//        Integer i = 1;
//        Double d = 1; // 错误,1 是 int 类型
    }
}

注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。

基本数据类型、包装类与字符串间的转换 ​

基本数据类型转为字符串 ​

方式 1,调用字符串重载的 valueOf() 方法。

java
int a = 10;
// String str = a; // 错误的

String str = String.valueOf(a);

方式 2,更直接的方式,使用字符串连接。

java
int b = 10;
// String str = a; // 错误的

String bstr = b + "";

字符串转为基本数据类型 ​

方式 1,除了 Character 类之外,其他的所有基本数据包装类都具有 parseXxx 静态方法可以将字符串参数转换为对应的基本类型,例如:

  • public static int parseInt(String s):将字符串参数转换为对应的 int 基本类型
  • public static long parseLong(String s):将字符串参数转换为对应的 long 基本类型
  • public static double parseDouble(String s):将字符串参数转换为对应的 double 基本类型

方式 2,字符串转为包装类,然后可以自动拆箱为基本数据类型:

  • public static Integer valueOf(String s):将字符串参数转换为对应的 Integer 包装类,然后可以自动拆箱为 int 基本类型
  • public static Long valueOf(String s):将字符串参数转换为对应的 Long 包装类,然后可以自动拆箱为 long 基本类型
  • public static Double valueOf(String s):将字符串参数转换为对应的 Double 包装类,然后可以自动拆箱为 double 基本类型

注意,如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出 java.lang.NumberFormatException 异常。

方式 3,通过包装类的构造器实现:

int i = new Integer("12");
java
package javacode.oop3.baozhuanglei;

public class Test5 {
    public static void main(String[] args) {
        // String -> 基础数据类型

        String strNum = "1998";

        // 字符串转 int
        int i = Integer.parseInt(strNum);

        // 字符串转 short
        short i1 = Short.parseShort(strNum);

        // 字符串转为基本数据类型的包装类
        Integer integer = Integer.valueOf(strNum);

        Integer integer1 = new Integer(strNum);
    }
}

总结 ​

包装类的其它 API ​

数据类型的最大最小值 ​

Integer.MAX_VALUE 和 Integer.MIN_VALUE
    
Long.MAX_VALUE 和 Long.MIN_VALUE
    
Double.MAX_VALUE 和 Double.MIN_VALUE

字符转大小写 ​

Character.toUpperCase('x');

Character.toLowerCase('X');

整数转进制 ​

Integer.toBinaryString(int i) 
    
Integer.toHexString(int i)
    
Integer.toOctalString(int i)

比较的方法 ​

Double.compare(double d1, double d2)
    
Integer.compare(int x, int y)

包装类对象的特点 ​

包装类缓存对象 ​

包装类缓存对象
Byte-128~127
Short-128~127
Integer-128~127
Long-128~127
Float无
Double无
Character0~127
Booleantrue 和 false
java
package javacode.oop3.baozhuanglei;

public class Test7 {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 1;
        // 缓存对象
        System.out.println(a == b); // true

        Integer i = 128;
        Integer j = 128;
        // 超出最大缓存范围 127
        System.out.println(i == j); // false

        Integer m = new Integer(1); // 新 new 的在堆中
        Integer n = 1; // 这个用的是缓冲的常量对象,在方法区
        System.out.println(m == n); // false

        Integer x = new Integer(1); // 新 new 的在堆中
        Integer y = new Integer(1); // 另一个新 new 的在堆中
        System.out.println(x == y); // false


        Double d1 = 1.0;
        Double d2 = 1.0;
        System.out.println(d1 == d2); // false 比较地址,因为没有缓存对象,每一个都是新 new 的
    }
}

类型转换问题 ​

java
package javacode.oop3.baozhuanglei;

import org.junit.Test;

public class Test8 {
    public static void main(String[] args) {
        Integer i = 1000;
        double j = 1000;
        // true 会先将 i 自动拆箱为 int,然后根据基本数据类型“自动类型转换”规则,转为 double 比较
        System.out.println(i==j);
    }
    @Test
    public void test() {
        Integer i = 1000;
        int j = 1000;
        System.out.println(i==j); // true 会自动拆箱,按照基本数据类型进行比较
    }
    @Test
    public void test2() {
        Integer i = 1;
        Double d = 1.0;
//        System.out.println(i==d);//编译报错
    }
}

包装类对象不可变 ​

java
package javacode.oop3.baozhuanglei;

public class Test9 {
    public static void main(String[] args) {
        int i = 1;
        Integer j = new Integer(2);
        Circle c = new Circle();
        change(i, j, c);
        System.out.println("i = " + i); // 1
        System.out.println("j = " + j); // 2
        System.out.println("c.radius = " + c.radius); // 10.0
    }

    /*
     * 方法的参数传递机制:
     * (1)基本数据类型:形参的修改完全不影响实参
     * (2)引用数据类型:通过形参修改对象的属性值,会影响实参的属性值
     * 这类 Integer 等包装类对象是“不可变”对象,即一旦修改,就是新对象,和实参就无关了
     */
    public static void change(int a, Integer b, Circle c) {
        a += 10;
//		b += 10;//等价于  b = new Integer(b+10);
        c.radius += 10;
		/*c = new Circle();
		c.radius+=10;*/
    }
}

class Circle {
    double radius;
}

练习 ​

如下两个题目输出结果相同吗?为什么?

java
package javacode.oop3.baozhuanglei;

import org.junit.Test;

public class Test10 {
    @Test
    public void test() {
        /**
         * 因为三元表达式要求表达式二和表达式三的类型相同,所以表达式二类型会类型提升为 Double,所以输出的是
         * double 类型的 1.0
         */
        Object o1 = true ? new Integer(1) : new Double(2.0);
        System.out.println(o1); // 1.0


        Object o2;
        if (true)
            o2 = new Integer(1);
        else
            o2 = new Double(2.0);
        System.out.println(o2); // 1
    }
}
java
package javacode.oop3.baozhuanglei;

public class Test11 {
    public static void main(String[] args) {
        Integer i = new Integer(1);
        Integer j = new Integer(1);
        System.out.println(i == j); // false,new 的对象会创建一个新的实例

        Integer m = 1;
        Integer n = 1;
        System.out.println(m == n); // true,因为被包装类缓存了

        Integer x = 128;
        Integer y = 128;
        System.out.println(x == y); // false,超出缓存最大范围
    }
}

利用 Vector 代替数组处理,从键盘读入学生成绩(以负数代表输入结束),找出最高分,并输出学生成绩等级。

TIP

  • 数组一旦创建,长度就固定不变,所以在创建数组前就需要知道它的长度。而向量类 java.util.Vector 可以根据需要动态伸缩
  • 创建 Vector 对象:Vector v=new Vector();
  • 给向量添加元素:v.addElement(Object obj); // obj必须是对象
  • 取出向量中的元素:Object obj=v.elementAt(0);。注意第一个元素的下标是 0,返回值类型是 Object
  • 计算向量的长度:v.size();
  • 若与最高分相差 10 分内,A 等;20 分内,B 等;30 分内,C 等;其它,D 等

Released under the MIT License.