关键字:static

回顾类中的实例变量(即非 static 的成员变量)

class Circle {
	private double radius;
 
	public Circle(double radius) {
        this.radius = radius;
	}
 
	public double calcArea() {
        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 变化不会影响 c2radius,反之亦然。

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

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

当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过 new 关键字才会产出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份。例如,所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

此外,在类中声明的实例方法,在类的外面必须要先创建对象,才能调用。但是有些方法的调用者和当前类的对象无关,这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。

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

static 关键字

使用范围:在 Java 类中,可用 static 修饰属性、方法、代码块、内部类

被修饰后的成员具备以下特点:

  • 随着类的加载而加载
  • 优先于对象存在
  • 修饰的成员,被所有对象所共享
  • 访问权限允许时,可不创建对象,直接被类调用

静态变量

使用 static 修饰的成员变量就是静态变量(或类变量、类属性)

格式:

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

特点:

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

对应的内存结构:(以经典的 JDK6 内存解析为例,此时静态变量存储在方法区)

静态方法

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

格式:

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

特点:

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

单例设计模式(Singleton)

设计模式概述

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

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

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

对软件设计模式的研究造就了一本可能是面向对象设计方面最有影响的书籍:《设计模式》(Design Patterns: Elements of Reusable Object-Oriented Software),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为”四人组(Gang of Four)“,而这本书也就被称为”四人组(或 GoF)“书。

何为单例模式

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

实现思路

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

两种实现方式

饿汉式

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

懒汉式

// 注意:该实现线程不安全,后续课程进行优化
class Singleton {
    // 1. 私有化构造器
    private Singleton() {
    }
 
    // 2. 内部提供一个当前类的实例(此实例也必须静态化)
    private static Singleton instance;
 
    // 3. 提供公共的静态的方法,返回当前类的对象
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

单例设计模式的线程安全问题

饿汉式 VS. 懒汉式

饿汉式:

  • 特点:立即加载,即在使用类的时候已经将对象创建完毕。
  • 优点:实现简单;没有多线程安全问题(依赖于 JVM 类的加载机制)。
  • 缺点:当类被加载的时候,会初始化 static 的实例,静态变量被创建并分配内存空间,从这以后,这个 static 的实例便一直占着这块内存,直到类被卸载时,静态变量被摧毁,并释放所占有的内存。类加载时,就要创建实例

懒汉式:

  • 特点:延迟加载,即在调用静态方法时实例才被创建。
  • 优点:当类被加载的时候,static 的实例未被创建并分配内存空间,当静态方法第一次被调用时,初始化实例变量,并分配内存。节约内存:没有获取过实例时,不占用内存
  • 缺点:实现较复杂(其实还是挺简单的)
  • 注意:在多线程环境中,这种实现方法线程不安全,根本不能保证单例的唯一性。
    • 多线程 章节 ,会将懒汉式改造成线程安全的模式。

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

由于单例模式只生成一个实例,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决

应用场景举例:

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

理解 main 方法的语法

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

又因为 main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情况,我们在之前的例子中多次碰到。

命令行参数用法举例:

public class CommandPara {
    public static void main(String[] args) {
        for (int i = 0; i < args.length; i++) {
            System.out.println("args[" + i + "] = " + args[i]);
        }
    }
}
# 运行程序 CommandPara.class
java CommandPara "Tom" "Jerry" "Shkstart"

输出结果:

args[0] = Tom
args[1] = Jerry
args[2] = Shkstart

IDEA 工具配置运行参数:

类的成员之四:代码块

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

代码块(或初始化块)的作用:对 Java 类或对象进行初始化

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

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

静态代码块

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

格式:

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

[修饰符] class 类名 {
	static {
        静态代码块
    }
}

特点:

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

非静态代码块

格式:

[修饰符] class 类名 {
    {
        非静态代码块
    }
}

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

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

执行特点:

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

小结:实例变量赋值顺序

  1. 声明成员变量的默认初始化
  2. 显式初始化、多个初始化块(按先后顺序依次执行)
  3. 构造器
  4. 通过“对象.属性”或“对象.方法”的方式,可多次给属性赋值

关键字:final

final:最终的,不可更改的

final 修饰类

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

例如:StringSystemStringBuffer

final 修饰方法

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

例如:Object 类中的 getClass()

final 修饰变量

final 修饰某个变量(成员变量或局部变量),一旦赋值,它的值就不能被修改,即常量。

常量名建议使用大写字母,多个单词用下划线隔开。

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

  • 修饰成员变量
    • 类(静态)成员变量
    • 实例(非静态)成员变量
  • 修饰局部变量
    • 函数参数
    • 普通局部变量

抽象类与抽象方法

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

举例:

我们声明一些几何图形类:圆、矩形、三角形等,发现这些类都有共同特征:求面积、求周长。那么这些共同特征应该抽取到一个共同父类:几何图形类中。但是这些方法在父类中又无法给出具体的实现,而是应该交给子类各自具体实现。那么父类在声明这些方法时,就只有方法签名,没有方法体,我们把没有方法体的方法称为抽象方法。Java 语法规定,包含抽象方法的类必须是抽象类。

格式:

抽象类:被 abstract 修饰的类

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

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

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

注意:抽象方法没有方法体

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,叫做方法实现

使用说明:

  1. 抽象类不能实例化(创建对象),如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。
    • 理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
    • 抽象类是用来被继承的,抽象类的子类必须重写父类的所有抽象方法,并提供方法体。若没有重写全部的抽象方法,则仍为抽象类。
  2. 抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。
    • 理解:子类的构造方法中,有默认的 super() 或手动的 super(实参列表),需要访问父类构造方法。
  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
    • 理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。
  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。
    • 理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

注意事项:

  • 不能用 abstract 修饰变量、代码块、构造器
  • 不能用 abstract 修饰私有方法、静态方法、final 的方法、final 的类
    • abstract:必须要重写方法/子类继承
    • 私有方法、静态方法、final 的方法、final 的类:不允许重写/继承

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

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

解决的问题:

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

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

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

接口(interface)

类比

生活中大家每天都在用 USB 接口,那么 USB 接口与我们今天要学习的接口有什么相同点呢?

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

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

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

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

概述

接口就是规范,定义的一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。继承是一个“是不是”的 is-a 关系,而接口实现则是“能不能”的 has-a 关系。

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

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

定义格式

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

引用数据类型:数组,类,枚举,接口,注解、记录

格式:

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

接口的成员说明

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

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

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

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

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

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

  1. 私有方法

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

接口的使用规则

类实现接口(implements)

接口不能实例化(创建对象),但是可以被类实现(implements,类似于被继承)。

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

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

注意:

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

类实现接口的多实现

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

格式:

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

接口中,有多个抽象方法时,实现类必须重写所有抽象方法(除了抽象类)。如果抽象方法有相同的函数签名(同名、同参数列表)的,只需要重写一次。

接口间的多继承(extends)

一个接口能继承另一个或者多个接口,接口的继承也使用 extends 关键字,子接口继承父接口的方法。

格式:

[修饰符] interface 子接口 extends 父接口1, 父接口2, 父接口3 {
 
}
  • 非抽象类实现接口:接口及接口的所有父接口的抽象方法都要重写。
  • 方法签名相同的抽象方法只需要实现一次。

小贴士:

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

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

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

使用接口的静态成员

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

使用接口的非静态方法

对于接口的静态方法,直接使用“接口名.”进行调用即可。也只能使用“接口名.”进行调用,不能通过实现类的对象进行调用(接口的静态方法不被继承)

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

冲突问题

默认方法冲突

类优先原则:当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法冲突(重名、函数签名也相同),子类就近选择执行父类的成员方法。

接口冲突(左右为难):

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

选择保留其中一个,通过“接口名.super.方法名”的方法选择保留哪个接口的默认方法

public class Person implements A, B {
    @Override
    public void sameFunc() {
        // (1) 保留其中一个父接口的
		// A.super.sameFunc();
		// B.super.sameFunc();
 
        // (2) 完全重写
        System.out.println("完全重写");
    }
}

常量冲突

属性没有类优先原则!

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

此时在子类中想要引用父类或父接口的同名的常量或成员变量时,就会有冲突问题。

  • super.成员变量名:父类的成员变量
  • 接口名.静态常量名:指定接口的静态常量

接口的总结与面试题

接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。

声明接口用 interface 关键字,接口的成员目前只支持 5 种:

  1. 公共的静态常量
  2. 公共的抽象方法
  3. 公共的默认方法(JDK8.0 及以上)
  4. 公共的静态方法(JDK8.0 及以上)
  5. 私有方法(JDK9.0 及以上)

类可以实现接口,关键字是 implements,而且支持多实现。如果实现类不是抽象类,就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后。

接口可以继承接口,关键字是 extends,而且支持多继承。

接口的默认方法可以选择重写或不重写。如果有冲突问题,另行处理。子类重写父接口的默认方法,要去掉 default,子接口重写父接口的默认方法,不要去掉 default

接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过“接口名.静态方法名”进行调用。

面试题:

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

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

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

  1. 为什么 JDK8.0 之后允许接口定义静态方法和默认方法呢?它违反了接口作为一个抽象标准定义的概念

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

默认方法:(1)我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会涉及到原来使用这些接口的类就会有问题,那么为了保持与旧版本代码的兼容性,只能允许在接口中定义默认方法实现。比如:Java8 中对 CollectionListComparator 等接口提供了丰富的默认方法。(2)当我们接口的某个抽象方法,在很多实现类中的实现代码是一样的,此时将这个抽象方法设计为默认方法更为合适,那么实现类就可以选择重写,也可以选择不重写。

  1. 为什么 JDK1.9 要允许接口定义私有方法呢?我们说接口是规范,规范是需要公开让大家遵守的

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

接口与抽象类之间的对比

No.区别点抽象类接口
1定义可以包含抽象方法的类主要是抽象方法和全局常量的集合
2组成构造器、抽象方法、普通方法、常量、变量常量、抽象方法、默认方法、静态方法
3使用子类继承抽象类(extends)子类实现接口(implements)
4关系抽象类可以实现多个接口接口不能继承抽象类,只能继承多个接口
5常用设计模式模版方法简单工厂、工厂方法、代理模式
6对象都不能实例化、只能通过对象的多态性产生实例对象
7局限抽象类有单继承的局限接口没有此局限
8实际作为一个模版作为一个标准、或是表示一种能力
9选择如果抽象类和接口都可以使用的话优先使用接口,避免单继承的局限

类的成员之五:内部类

概述

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

为什么要声明内部类?

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

总的来说,遵循“高内聚、低耦合”的面向对象开发原则。

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

成员内部类

如果成员内部类中不使用外部类的非静态成员,那么通常将内部类声明为静态内部类,否则声明为非静态内部类

格式:

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

成员内部类的使用特征,概括来讲有如下两种角色:

  1. 成员内部类作为类的成员的角色:
    • 和外部类不同,Inner class 还可以声明为 privateprotected
    • 可以调用外部类的结构。(注意:在静态内部类中不能使用外部类的非静态成员)
    • Inner class 可以声明为 static 的,但此时就不能再使用外层类的非 static 的成员变量;
  2. 成员内部类作为类的角色:
    • 可以在内部定义属性、方法、构造器等结构
    • 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
    • 可以声明为 abstract 类 ,因此可以被其它的内部类继承
    • 可以声明为 final 的,表示不能被继承
    • 编译以后生成 OuterClass$InnerClass.class 字节码文件(也适用于局部内部类)

注意点:

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

创建成员内部类对象

实例化静态内部类:

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

实例化非静态内部类:

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

局部内部类

非匿名局部内部类

格式:

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

说明:

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

匿名内部类

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

new 父类([实参列表]) {
    重写方法...
}

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

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

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

interface A {
	void a();
}
 
public class Test {
    public static void main(String[] args) {
    	A obj = new A() {
			@Override
			public void a() {
				System.out.println("aaaa");
			}
    	};
    	obj.a();
    }
}

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

interface A {
	void method();
}
 
public class Test {
    public static void test(A a) {
    	a.method();
    }
 
    public static void main(String[] args) {
    	test(new A() {
			@Override
			public void method() {
				System.out.println("aaaa");
			}
    	});
    }
}

枚举类

概述

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

枚举类的例子举不胜举:

  • 星期:Monday(星期一)、……、Sunday(星期天)
  • 性别:Man(男)、Woman(女)
  • 月份:January(1 月)、……、December(12 月)
  • 季节: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 之前如何声明枚举类呢?

  1. 私有化类的构造器,保证不能在类的外部创建其对象
  2. 在类的内部创建枚举类的实例。声明为:public static final,对外暴露这些常量对象
  3. 对象如果有实例变量,应该声明为 private final(建议,不是必须),并在构造器中初始化

示例代码:

class Season {
    private final String NAME; // 季节的名称
    private final String DESC; // 季节的描述
 
    private Season(String name, String desc) {
        this.NAME = name;
        this.DESC = 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 "Season{" +
                "NAME='" + NAME + '\'' +
                ", DESC='" + DESC + '\'' +
                '}';
    }
}
 
class SeasonTest {
    public static void main(String[] args) {
        System.out.println(Season.AUTUMN);
    }
}

定义枚举类(JDK5.0 之后)

使用 enum 关键字:

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

举例 1:

package com.atguigu.enumeration;
 
public enum Week {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY;
}
public class TestEnum {
	public static void main(String[] args) {
		Week w = Week.SATURDAY;
		System.out.println(w); // SATURDAY
 
		switch (w) {
            case MONDAY:
                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("开始盼周末,无心学习");break;
            case SUNDAY:
                System.out.println("一觉到下午");break;
        }
	}
}

举例 2:

public enum SeasonEnum {
    SPRING("春天", "春风又绿江南岸"),
    SUMMER("夏天", "映日荷花别样红"),
    AUTUMN("秋天", "秋水共长天一色"),
    WINTER("冬天", "窗含西岭千秋雪");
 
    private final String seasonName;
    private final String seasonDesc;
 
    private SeasonEnum(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
 
    public String getSeasonName() {
        return seasonName;
    }
 
    public String getSeasonDesc() {
        return seasonDesc;
    }
}

enum 方式定义的要求和特点:

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

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

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(参数) {
        //抽象方法的实现或重写
    },
    // ...
}

举例:

interface Info {
	void show();
}
 
// 使用enum关键字定义枚举类
enum Season 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("冬天", "白雪皑皑") {
		public void show() {
			System.out.println("2002 年的第一场雪");
		}
	};
	
	// 2. 声明每个对象拥有的属性: private final 修饰
	private final String SEASON_NAME;
	private final String SEASON_DESC;
	
	// 3. 私有化类的构造器
	private Season(String seasonName, String seasonDesc) {
		this.SEASON_NAME = seasonName;
		this.SEASON_DESC = seasonDesc;
	}
	
	public String getName() {
		return SEASON_NAME;
	}
 
	public String getDesc() {
		return SEASON_DESC;
	}
}

注解

概述

什么是注解

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

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

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

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

注解与注释

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

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

注解的重要性

在 JavaSE 中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在 JavaEE/Android 中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替 JavaEE 旧版中所遗留的繁冗代码 和 XML 配置等。

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

常见的注解

生成文档相关的注解

@author // 标明开发该类模块的作者,多个作者之间使用,分割
@version // 标明该类模块的版本
@see // 参考转向,也就是相关主题
@since // 从哪个版本开始增加的
@param // 对方法中某参数的说明,如果没有参数就不能写
@return // 对方法返回值的说明,如果方法的返回值类型是 void 就不能写
@exception // 对方法可能抛出的异常进行说明 ,如果方法没有用 throws 显式抛出的异常就不能写

在编译时进行格式检查

JDK 内置的三个基本注解

  1. @Override:限定重写父类方法,该注解只能用于方法
    • 用于检测被标记的方法为有效的重写方法,如果不是,则报编译错误!
    • 只能标记在方法上
    • 它会被编译器程序读取
  2. @Deprecated:用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
    • 用于表示被标记的数据已经过时,不推荐使用
    • 可以用于修饰属性、方法、构造、类、包、局部变量、参数
    • 它会被编译器程序读取
  3. @SuppressWarnings:抑制编译器警告
    • 抑制编译警告。当我们不希望看到警告信息的时候,可以使用 SuppressWarnings 注解来抑制警告信息
    • 可以用于修饰类、属性、方法、构造、局部变量、参数
    • 它会被编译器程序读取
    • 可以指定的警告类型有:(了解)
      • all:抑制所有警告
      • unchecked:抑制与未检查的作业相关的警告
      • unused:抑制与未用的程式码及停用的程式码相关的警告
      • deprecation:抑制与淘汰的相关警告
      • nls:抑制与非 nls 字串文字相关的警告
      • null:抑制与空值分析相关的警告
      • rawtypes:抑制与使用 raw 类型相关的警告
      • static-access:抑制与静态存取不正确相关的警告
      • static-method:抑制与可能宣告为 static 的方法相关的警告
      • super:抑制与置换方法相关但不含 super 呼叫的警告

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

Servlet 3.0 提供了注解,使得不再需要在 web.xml 文件中进行 Servlet 的配置:

@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);
	}
}
<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);
}
<!-- 配置事务属性 -->
<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>

元注解

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

  1. @Target:用于描述注解的使用范围
    • 可以通过枚举类型 ElementType 的 10 个常量对象来指定
    • TYPEMETHODCONSTRUCTORPACKAGE、……
  2. @Retention:用于描述注解的生命周期
    • 可以通过枚举类型 RetentionPolicy 的 3个 常量对象来指定
    • SOURCE(源代码)、CLASS(字节码)、RUNTIME(运行时)
    • 唯有 RUNTIME 阶段才能被反射读取到。
  3. @Documented:表明这个注解应该被 javadoc 工具记录
  4. @Inherited:允许子类继承父类中的注解

示例:

package java.lang;
 
import java.lang.annotation.*;
 
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
package java.lang;
 
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
 
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}
package java.lang;
 
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
 
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

自定义注解

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

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

声明自定义注解

格式:

[元注解]
[修饰符] @interface 注解名 {
	[成员列表]
}

说明:

  • 自定义注解可以通过 四个元注解,分别说明它的声明周期,使用位置,是否被继承,是否被生成到 API 文档中
  • Annotation 的成员在 Annotation 定义中以无参数有返回值的抽象方法的形式来声明,我们又称为配置参数。返回值类型只能是八种基本数据类型、String 类型、Class 类型、enum 类型、Annotation 类型、以上所有类型的数组
  • 可以使用 default 关键字为抽象方法指定默认返回值
  • 如果定义的注解含有抽象方法,那么使用时必须指定返回值,除非它有默认值。格式是“方法名 = 返回值”,如果只有一个抽象方法需要赋值,且方法名为 value,可以省略“value=”,所以如果注解只有一个抽象方法成员,建议使用方法名 value

举例:

package com.atguigu.annotation;
 
import java.lang.annotation.*;
 
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
    String value();
}
package com.atguigu.annotation;
 
import java.lang.annotation.*;
 
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    String columnName();
    String columnType();
}

使用自定义注解

package com.atguigu.annotation;
 
@Table("t_stu")
public class Student {
    @Column(columnName = "sid", columnType = "int")
    private int id;
    @Column(columnName = "sname", columnType = "varchar(20)")
    private String name;
 
	// getter/setter
}

读取和处理自定义注解

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

我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是 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

第 1 步:在项目中 File -> Project Structure 中操作,添加 Libraries

其中,junit-libs 包内容如下:

第 2 步:选择要在哪些 module 中应用 JUnit

第 3 步:检查是否应用成功

注意 Scope:选择 Compile,否则编译时,无法使用 JUnit

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

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

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

  • 所在的类必须是 public 的,非抽象的,包含唯一的无参构造器。
  • @Test 标记的方法本身必须是 public,非抽象的,非静态的,void 无返回值,() 无参数的。

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

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

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

-Deditable.java.test.console=true

配置文件位置:

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

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

定义 test 测试方法模板

定义 test 测试方法模板

包装类

为什么需要包装类

Java 提供了两个类型系统:基本数据类型与引用数据类型。使用基本数据类型在于效率,然而当要使用只针对对象设计的 API 或新特性(例如泛型),怎么办呢?例如:

// 情况 1:方法形参
Object 类的 equals(Object obj)
 
// 情况 2:方法形参
ArrayList 类的 add(Object obj)
// 没有如下的方法:
add(int number)
add(double d)
add(boolean b)
 
// 情况 3:泛型
Set<T>
List<T>
Cllection<T>
Map<K, V>

有哪些包装类

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

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

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 和特性

Integer obj1 = new Integer(4); // 使用构造函数函数
Float f = new Float("4.56");
Long l = new Long("asdf"); // NumberFormatException
 
Integer obj2 = Integer.valueOf(4); // 使用包装类中的 valueOf 方法

拆箱

拆箱:把包装类对象转为基本数据类型(包装对象 基本数值)

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

Integer obj = new Integer(4);
int num1 = obj.intValue();

自动装箱与拆箱

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

Integer i = 4; // 自动装箱,相当于 Integer i = Integer.valueOf(4);
i = i + 5; // 等号右边:将 i 对象转成基本数值(自动拆箱),相当于 i.intValue() + 5;
// 加法运算完成后,再次装箱,把基本数值转成包装对象

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

Integer i = 1;
Double d = 1.0; // 正确
// Double d = 1; // 错误的,1 是 int 类型

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

基本数据类型转为字符串

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

int a = 10;  
// String str = a; // 错误的  
 
String str = String.valueOf(a);

方式 2:更直接的方式

int a = 10;  
 
String str = "" + a;

字符串转为基本数据类型

方式 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 a = Integer.parseInt("整数的字符串");
double d = Double.parseDouble("小数的字符串");
boolean b = Boolean.parseBoolean("true或false");
 
int a = Integer.valueOf("整数的字符串");
double d = Double.valueOf("小数的字符串");
boolean b = Boolean.valueOf("true或false");
 
int a = new Integer("整数的字符串");
double d = new Double("小数的字符串");
boolean b = new Boolean("true或false");

小结

包装类的其它 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
Boolean ttruefalsee
Integer a = 1;
Integer b = 1;
System.out.println(a == b); // true
 
Integer i = 128;
Integer j = 128;
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 的

类型转换问题

Integer i = 1000;
double j = 1000;
System.out.println(i == j); // true
// 会先将 i 自动拆箱为 int,然后根据基本数据类型“自动类型转换”规则,转为 double 比较
Integer i = 1000;
int j = 1000;
System.out.println(i == j); // true
// 会自动拆箱,按照基本数据类型进行比较
Integer i = 1;
Double d = 1.0;
System.out.println(i == d); // 编译报错

包装类对象不可变

public class TestExam {
	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;
}

题目

笔试题:如下两个题目输出结果相同吗?各是什么?

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

面试题:

public void method1() {
    Integer i = new Integer(1);
    Integer j = new Integer(1);
    System.out.println(i == j); // false
 
    Integer m = 1;
    Integer n = 1;
    System.out.println(m == n); // true
 
    Integer x = 128;
    Integer y = 128;
    System.out.println(x == y); // false
}