# 第 08 章_面向对象编程 (高级)
讲师:尚硅谷 - 宋红康(江湖人称:康师傅)
官网:http://www.atguigu.com
# 本章专题与脉络
# 1. 关键字:static
回顾类中的实例变量(即非 static 的成员变量)
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 修饰即可,称为类变量(或类属性)!
# 1.1 类属性、类方法的设计思想
当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过 new 关键字才会产出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下, 某些特定的数据在内存空间里只有一份
。例如,所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。
此外,在类中声明的实例方法,在类的外面必须要先创建对象,才能调用。但是有些方法的调用者和当前类的对象无关,这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。
这里的类变量、类方法,只需要使用 static
修饰即可。所以也称为静态变量、静态方法。
# 1.2 static 关键字
使用范围:
- 在 Java 类中,可用 static 修饰属性、方法、代码块、内部类
被修饰后的成员具备以下特点:
- 随着类的加载而加载
- 优先于对象存在
- 修饰的成员,被所有对象所共享
- 访问权限允许时,可不创建对象,直接被类调用
# 1.3 静态变量
# 1.3.1 语法格式
使用 static 修饰的成员变量就是静态变量(或类变量、类属性)
[修饰符] class 类{ | |
[其他修饰符] static 数据类型 变量名; | |
} |
# 1.3.2 静态变量的特点
静态变量的默认值规则和实例变量一样。
静态变量值是所有对象共享。
静态变量在本类中,可以在任意方法、代码块、构造器中直接使用。
如果权限修饰符允许,在其他类中可以通过 “
类名.静态变量
” 直接访问,也可以通过 “对象.静态变量
” 的方式访问(但是更推荐使用类名。静态变量的方式)。静态变量的 get/set 方法也静态的
当局部变量与静态变量
重名时
,使用 “类名.静态变量
” 进行区分。
# 1.3.3 区分:实例变量 和 静态变量
个数
- 实例变量:类的每一个实例都存有一份;
- 静态变量:在内存空间中只有一份,被类的所有实例所共享;
内存位置
- 实例变量:堆空间的实例实体中;
- 静态变量:
- JDK6 及之前,存放在方法区;
- JDK7 及之后,存放在堆空间;
加载 / 消亡时机
- 实例变量:随着实例的创建而加载;随着实例的消失而消失;
- 静态变量:随着类的加载而加载,而类只会加载一次;随着类的卸载而消失;
调用者
- 实例变量:实例对象
- 静态变量:类 / 实例对象
判断是否可以调用(从生命周期的角度解释)
静态变量 实例变量 类 √ × 实例对象 √ √
# 1.3.4 举例
举例 1:
class Chinese{ | |
// 实例变量 | |
String name; | |
int age; | |
// 类变量 | |
static String nation;// 国籍 | |
public Chinese() { | |
} | |
public Chinese(String name, int age) { | |
this.name = name; | |
this.age = age; | |
} | |
@Override | |
public String toString() { | |
return "Chinese{" + | |
"name='" + name + '\'' + | |
", age=" + age + | |
", nation='" + nation + '\'' + | |
'}'; | |
} | |
} | |
public class StaticTest { | |
public static void main(String[] args) { | |
Chinese c1 = new Chinese("康师傅",36); | |
c1.nation = "中华人民共和国"; | |
Chinese c2 = new Chinese("老干妈",66); | |
System.out.println(c1); | |
System.out.println(c2); | |
System.out.println(Chinese.nation); | |
} | |
} |
静态变量的存储位置:
以经典的
JDK6
内存解析为例,此时存储在方法区;自
JDK7
开始,存储在堆空间中!
举例 2:
package com.atguigu.keyword; | |
public class Employee { | |
private static int total;// 这里私有化,在类的外面必须使用 get/set 方法的方式来访问静态变量 | |
static String company; // 这里缺省权限修饰符,是为了方便类外以 “类名。静态变量” 的方式访问 | |
private int id; | |
private String name; | |
public Employee() { | |
total++; | |
id = total;// 这里使用 total 静态变量的值为 id 属性赋值 | |
} | |
public Employee(String name) { | |
this(); | |
this.name = name; | |
} | |
public void setId(int id) { | |
this.id = id; | |
} | |
public int getId() { | |
return id; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public static int getTotal() { | |
return total; | |
} | |
public static void setTotal(int total) { | |
Employee.total = total; | |
} | |
@Override | |
public String toString() { | |
return "Employee{company = " + company + ",id = " + id + " ,name=" + name +"}"; | |
} | |
} |
package com.atguigu.keyword; | |
public class TestStaticVariable { | |
public static void main(String[] args) { | |
// 静态变量 total 的默认值是 0 | |
System.out.println("Employee.total = " + Employee.getTotal()); | |
Employee e1 = new Employee("张三"); | |
Employee e2 = new Employee("李四"); | |
System.out.println(e1);// 静态变量 company 的默认值是 null | |
System.out.println(e2);// 静态变量 company 的默认值是 null | |
System.out.println("Employee.total = " + Employee.getTotal());// 静态变量 total 值是 2 | |
Employee.company = "尚硅谷"; | |
System.out.println(e1);// 静态变量 company 的值是尚硅谷 | |
System.out.println(e2);// 静态变量 company 的值是尚硅谷 | |
// 只要权限修饰符允许,虽然不推荐,但是也可以通过 “对象。静态变量” 的形式来访问 | |
e1.company = "超级尚硅谷"; | |
System.out.println(e1);// 静态变量 company 的值是超级尚硅谷 | |
System.out.println(e2);// 静态变量 company 的值是超级尚硅谷 | |
} | |
} |
# 1.3.5 内存解析
# 1.4 静态方法
# 1.4.1 语法格式
用 static 修饰的成员方法就是静态方法。
[修饰符] class 类{ | |
[其他修饰符] static 返回值类型 方法名(形参列表){ | |
方法体 | |
} | |
} |
# 1.4.2 静态方法的特点
静态方法在本类的任意方法、代码块、构造器中都可以直接被调用。
只要权限修饰符允许,静态方法在其他类中可以通过 “类名。静态方法 “的方式调用。也可以通过” 对象。静态方法 “的方式调用(但是更推荐使用类名。静态方法的方式)。
在 static 方法内部只能访问类的 static 修饰的属性或方法,不能访问类的非 static 的结构。
结合生命周期,访问非 static 的属性 / 方法,相当于前面省略了
this.
,即当前实例对象的引用!而静态方法是在类加载的时候就调用了,此时还没有创建实例对象,自然也无法访问啦~
静态方法可以被子类继承,但不能被子类重写。
静态方法的调用都只看编译时类型。
因为不需要实例就可以访问 static 方法,因此static 方法内部不能有 this,也不能有 super。如果有重名问题,使用 “类名.” 进行区别。
见上一条注释
问题:开发中,什么时候需要将属性声明为静态的?
- 判断当前类的多个实例是否能共享此成员变量,且值是相同的
- 开发中,常将一些常量声明为静态的。比如:Math.PI
问题:开发中,什么时候需要将方法声明为静态的?
- 方法内操作的变量如果都是静态变量(而非实例变量)的话,建议将方法声明为静态的
- 开发中,常将 **工具类中的方法** 声明为静态的。比如:Arrays 类、Math 类
# 1.4.3 举例
package com.atguigu.keyword; | |
public class Father { | |
public static void method(){ | |
System.out.println("Father.method"); | |
} | |
public static void fun(){ | |
System.out.println("Father.fun"); | |
} | |
} |
package com.atguigu.keyword; | |
public class Son extends Father{ | |
// @Override // 尝试重写静态方法,加上 @Override 编译报错,去掉 Override 不报错,但是也不是重写 | |
public static void fun(){ | |
System.out.println("Son.fun"); | |
} | |
} |
package com.atguigu.keyword; | |
public class TestStaticMethod { | |
public static void main(String[] args) { | |
Father.method(); | |
Son.method();// 继承静态方法 | |
Father f = new Son(); | |
f.method();// 执行 Father 类中的 method | |
} | |
} |
# 1.5 练习
笔试题:如下程序执行会不会报错
/** | |
* @author 尚硅谷 - 宋红康 | |
* @create 14:30 | |
*/ | |
public class StaticTest { | |
public static void main(String[] args) { | |
Demo test = null; | |
test.hello(); | |
} | |
} | |
class Demo{ | |
public static void hello(){ | |
System.out.println("hello!"); | |
} | |
} |
练习:
编写一个类实现银行账户的概念,包含的属性有 “帐号”、“密码”、“存款余额”、“利率”、“最小余额”,定义封装这些属性的方法。 账号要自动生成。
编写主类,使用银行账户类,输入、输出 3 个储户的上述信息。
考虑:哪些属性可以设计成 static 属性。
# 2. 单例 (Singleton) 设计模式
static 的一种应用
# 2.1 设计模式概述
**设计模式** 是在大量的 实践中总结
和 理论化
之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱。"套路"
经典的设计模式共有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)" 书。
# 2.2 何为单例模式
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
# 2.3 实现思路
如果我们要让类在一个虚拟机中只能产生一个对象:
- 首先必须将
类的构造器的访问权限设置为private
,这样,就不能用 new 操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象; - 因为在类的外部开始还无法得到类的对象,只能调用该类的某个
静态方法
以返回类内部创建的对象; - 静态方法只能访问类中的静态成员变量,所以,指向类内部产生的
该类对象的变量也必须定义成静态的
;
# 2.4 单例模式的两种实现方式
# 2.4.1 饿汉式
class Singleton { | |
// 1. 私有化构造器 | |
private Singleton() { | |
} | |
// 2. 内部提供一个当前类的实例 | |
// 4. 此实例也必须静态化 | |
private static Singleton single = new Singleton(); | |
// 3. 提供公共的静态的方法,返回当前类的对象 | |
public static Singleton getInstance() { | |
return single; | |
} | |
} |
# 2.4.2 懒汉式
class Singleton { | |
// 1. 私有化构造器 | |
private Singleton() { | |
} | |
// 2. 内部提供一个当前类的实例 | |
// 4. 此实例也必须静态化 | |
private static Singleton single; | |
// 3. 提供公共的静态的方法,返回当前类的对象 | |
public static Singleton getInstance() { | |
if(single == null) { | |
single = new Singleton(); | |
} | |
return single; | |
} | |
} |
# 2.4.3 饿汉式 vs 懒汉式
饿汉式:
- 特点:
立即加载
,即在使用类的时候已经将对象创建完毕。 - 优点:实现起来
简单
;没有多线程安全问题。 - 缺点:当类被加载的时候,会初始化 static 的实例,静态变量被创建并分配内存空间,从这以后,这个 static 的实例便一直占着这块内存,直到类被卸载时,静态变量被摧毁,并释放所占有的内存。因此在某些特定条件下会
耗费内存
。
懒汉式:
- 特点:
延迟加载
,即在调用静态方法时实例才被创建。 - 优点:实现起来比较简单;当类被加载的时候,static 的实例未被创建并分配内存空间,当静态方法第一次被调用时,初始化实例变量,并分配内存,因此在某些特定条件下会
节约内存
。 - 缺点:在多线程环境中,这种实现方法是完全错误的,
线程不安全
,根本不能保证单例的唯一性。- 说明:在多线程章节,会将懒汉式改造成线程安全的模式。
# 2.5 单例模式的优点及应用场景
由于单例模式只生成一个实例,减少了 系统性能开销
,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
举例:
应用场景
Windows 的 Task Manager (任务管理器) 就是很典型的单例模式
Windows 的 Recycle Bin (回收站) 也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
Application 也是单例的典型应用
应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只
能有一个实例去操作,否则内容不好追加。
数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
# 3. 理解 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.java | |
java CommandPara "Tom" "Jerry" "Shkstart" |
// 输出结果 | |
args[0] = Tom | |
args[1] = Jerry | |
args[2] = Shkstart |
IDEA 工具:
(1)配置运行参数
(2)运行程序
笔试题:
// 此处,Something 类的文件名叫 OtherThing.java | |
class Something { | |
public static void main(String[] something_to_do) { | |
System.out.println("Do something ..."); | |
} | |
} | |
// 上述程序是否可以正常编译、运行? |
# 4. 类的成员之四:代码块
主线一:属性、方法、构造器、代码库、(内部类)
主线二:封装、继承、多态
主线三:其他关键字,例如 final、abstract、interface 等
如果成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值,该怎么办呢?此时,可以考虑 **代码块(或初始化块)**。
代码块 (或初始化块) 的
作用
:- 对 Java 类或对象的成员变量进行初始化
代码块 (或初始化块) 的
分类
:静态代码块:一个类中代码块若有修饰符,则只能被 static 修饰
非静态代码块:没有使用 static 修饰的
# 4.1 静态代码块
如果想要为静态变量初始化,可以直接在静态变量的声明后面直接赋值,也可以使用静态代码块。
# 4.1.1 语法格式
在代码块的前面加 static,就是静态代码块。
【修饰符】 class 类{ | |
static{ | |
静态代码块 | |
} | |
} |
# 4.1.2 静态代码块的特点
随着类的加载而执行
由于类只会加载一次,因此静态代码块也只执行一次
作用:初始化类的信息
内部可以声明变量、调用属性 / 方法、编写输出语句等
先于非静态代码块的执行
从二者的生命周期来解释:类的加载一定在实例对象创建之前
多个静态代码块按照先后顺序执行
内部只能调用静态的结构(静态属性、静态方法),不能调用非静态的结构
同样从生命周期的角度来解释:类加载的时候还没有对象实例化呢,怎么可能去调用实例属性 / 实例方法呢?
package com.atguigu.keyword; | |
public class Chinese { | |
// private static String country = "中国"; | |
private static String country; | |
private String name; | |
{ | |
System.out.println("非静态代码块,country = " + country); | |
} | |
static { | |
country = "中国"; | |
System.out.println("静态代码块"); | |
} | |
public Chinese(String name) { | |
this.name = name; | |
} | |
} |
package com.atguigu.keyword; | |
public class TestStaticBlock { | |
public static void main(String[] args) { | |
Chinese c1 = new Chinese("张三"); | |
Chinese c2 = new Chinese("李四"); | |
} | |
} |
# 4.2 非静态代码块
# 4.2.1 语法格式
【修饰符】 class 类{ | |
{ | |
非静态代码块 | |
} | |
【修饰符】 构造器名(){ | |
// 实例初始化代码 | |
} | |
【修饰符】 构造器名(参数列表){ | |
// 实例初始化代码 | |
} | |
} |
# 4.2.2 非静态代码块的作用
和构造器一样,也是用于实例变量的初始化等操作。
# 4.2.3 非静态代码块的意义
如果多个重载的构造器有公共代码,并且这些代码都是先于构造器其他代码执行的,那么可以将这部分代码抽取到非静态代码块中,减少冗余代码。
# 4.2.4 非静态代码块的执行特点
- 随着实例对象的创建而执行
- 每创建一个实例对象,就执行一次。且先于构造器执行
- 作用:初始化对象的信息
- 内部可以声明变量、调用属性 / 方法、编写输出语句等
- 多个非静态代码块按照先后顺序执行
- 内部可以调用静态的结构(静态属性、静态方法),也可以调用非静态的结构
# 4.3 举例
举例 1:
(1)声明 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”
(2)编写测试类,测试类 main 方法的代码如下:
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()); | |
} |
如果不用非静态代码块,User 类是这样的:
package com.atguigu.block.no; | |
public class User { | |
private String username; | |
private String password; | |
private long registrationTime; | |
public User() { | |
System.out.println("新用户注册"); | |
registrationTime = System.currentTimeMillis(); | |
username = registrationTime+""; | |
password = "123456"; | |
} | |
public 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; | |
} | |
} |
如果提取构造器公共代码到非静态代码块,User 类是这样的:
package com.atguigu.block.use; | |
public class User { | |
private String username; | |
private String password; | |
private long registrationTime; | |
{ | |
System.out.println("新用户注册"); | |
registrationTime = System.currentTimeMillis(); | |
} | |
public User() { | |
username = registrationTime+""; | |
password = "123456"; | |
} | |
public User(String username, String password) { | |
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; | |
} | |
} |
举例 2:
private static DataSource dataSource = null; | |
static{ | |
InputStream is = null; | |
try { | |
is = DBCPTest.class.getClassLoader().getResourceAsStream("dbcp.properties"); | |
Properties pros = new Properties(); | |
pros.load(is); | |
// 调用 BasicDataSourceFactory 的静态方法,获取数据源。 | |
dataSource = BasicDataSourceFactory.createDataSource(pros); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
}finally{ | |
if(is != null){ | |
try { | |
is.close(); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} | |
} |
# 4.4 小结:实例变量(非静态属性)赋值顺序
问 1:(超纲)关于字节码文件中 <init>
的简单说明
- 构造器在编译后以
<init>
方法的方式呈现,因此每个<init>
方法对应一个构造器 <init>
方法内部的代码包含了实例变量的显示赋值、代码块中的赋值、构造器中的代码<init>
方法是用来初始化当前创建的实例对象的信息的
问 2:开发中,如何选择给实例变量赋值的位置?
- 显示赋值:适合属性值都相同的
- 构造器中赋值:适合属性值不同的
# 4.5 练习:分析加载顺序
# 练习 1
类的加载顺序:
先加载父类,再加载子类,在加载类模板的时候就执行了静态代码块。
结合 override,子类可以覆盖父类中同名同参数的方法,因此子类的加载一定在父类之后!
先调用子类的构造器,因为第一行一定是
this(..)
或super(..)
,通过super(..)
作为桥梁,调用父类的构造器,一层一层往上走,最终调用 java.lang.Object 类的构造器!再执行非静态代码块,最后执行构造器。
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("尚硅谷"); | |
System.out.println("Leaf的构造器"); | |
} | |
} | |
public class LeafTest{ | |
public static void main(String[] args){ | |
new Leaf(); | |
//new Leaf(); | |
} | |
} |
运行结果:
Root的静态初始化块 | |
Mid的静态初始化块 | |
Leaf的静态初始化块 | |
Root的普通初始化块 | |
Root的无参数的构造器 | |
Mid的普通初始化块 | |
Mid的无参数的构造器 | |
Mid的带参数构造器,其参数值:尚硅谷 | |
Leaf的普通初始化块 | |
Leaf的构造器 |
# 练习 2
class HelloA { | |
public HelloA() { | |
System.out.println("HelloA"); // 4 | |
} | |
{ | |
System.out.println("I'm A class"); // 3 | |
} | |
static { | |
System.out.println("static A"); // 1 | |
} | |
} | |
class HelloB extends HelloA { | |
public HelloB() { | |
System.out.println("HelloB"); // 6 | |
} | |
{ | |
System.out.println("I'm B class"); // 5 | |
} | |
static { | |
System.out.println("static B"); // 2 | |
} | |
} | |
public class Test01 { | |
public static void main(String[] args) { | |
System.out.println("-------start-------"); | |
new HelloB(); | |
// new HelloB(); | |
System.out.println("-------end-------"); | |
} | |
} |
运行结果:
-------start------- | |
static A | |
static B | |
I'm A class | |
HelloA | |
I'm B class | |
HelloB | |
-------end------- |
# 练习 3
public class Test02 { | |
static int x, y, z; | |
static { | |
int x = 5; // 这是局部变量! | |
x--; | |
} | |
static { | |
x--; | |
} | |
public static void method() { | |
y = z++ + ++z; | |
} | |
public static void main(String[] args) { | |
System.out.println("x = " + x); // x = -1 | |
z--; // z = -1 | |
method(); // y = 0 , z = 1 | |
System.out.println("result:" + (z + y + ++z)); // result:3 | |
} | |
} |
运行结果:
x = -1 | |
result:3 |
# 练习 4
class Base { | |
Base() { | |
method(100); // 调用的是子类重写的方法!!! | |
} | |
{ | |
System.out.println("base"); | |
} | |
public void method(int i) { | |
System.out.println("base:" + i); | |
} | |
} | |
class Sub extends Base { | |
Sub() { | |
super.method(70); // 明确调用父类的方法! | |
} | |
{ | |
System.out.println("sub"); | |
} | |
@Override | |
public void method(int j) { | |
System.out.println("sub:" + j); | |
} | |
} | |
public class Test03 { | |
public static void main(String[] args) { | |
Sub sub = new Sub(); | |
} | |
} |
运行结果:
base | |
sub:100 | |
sub | |
base:70 |
# 练习 5
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(); | |
} | |
} |
# 练习 6
package com.atguigu05.field.interview; | |
/** | |
* @author 尚硅谷 - 宋红康 | |
* @create 16:04 | |
*/ | |
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; | |
} | |
} |
# 5. final 关键字
# 5.1 final 的意义
final:最终的,不可更改的。可以用来修饰类、方法、变量。
# 5.2 final 的使用
# 5.2.1 final 修饰类
表示这个类不能被继承,没有子类。提高安全性,提高程序的可读性。
例如:String 类、System 类、StringBuffer 类
final class Eunuch{// 太监类 | |
} | |
class Son extends Eunuch{// 错误 | |
} |
# 5.2.2 final 修饰方法
表示这个方法不能被子类重写。
例如:Object 类中的 getClass ()
class Father{ | |
public final void method(){ | |
System.out.println("father"); | |
} | |
} | |
class Son extends Father{ | |
public void method(){// 错误 | |
System.out.println("son"); | |
} | |
} |
# 5.2.3 final 修饰变量
final 修饰某个变量(成员变量或局部变量),一旦赋值,它的值就不能被修改,即常量,常量名建议使用大写字母。
例如:final double MY_PI = 3.14;
修饰成员变量
如果某个成员变量用 final 修饰后,没有 set 方法,并且必须初始化(可以显式赋值、或在初始化块赋值、实例变量还可以在构造器中赋值)
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); | |
} | |
} |
- 修饰局部变量:
public class TestFinal { | |
public static void main(String[] args){ | |
final int MIN_SCORE ; | |
MIN_SCORE = 0; | |
final int MAX_SCORE = 100; | |
MAX_SCORE = 200; // 非法 | |
} | |
} |
- 错误演示:
class A { | |
private final String INFO = "atguigu"; // 声明常量 | |
public void print() { | |
//The final field A.INFO cannot be assigned | |
//INFO = "尚硅谷"; | |
} | |
} |
# 5.3 final 与 static 的搭配
修饰成员变量时,兼具二者特点,称为全局常量。
# 5.4 笔试题
题 1:排错
public class Something { | |
public int addOne(final int x) { | |
return ++x; // 报错,因为给 final 类型的变量 x 赋值了 | |
//return x + 1; // 不会报错,因为没有修改 x 的值 | |
} | |
} |
题 2:排错
public class Something { | |
public static void main(String[] args) { | |
Other o = new Other(); | |
new Something().addOne(o); | |
} | |
public void addOne(final Other o) { // 这里 final 修饰的是 Other 类的对象变量 o | |
//o = new Other (); // 报错 | |
o.i++; // 不会报错 | |
} | |
} | |
class Other { | |
public int i; | |
} |
# 6. 抽象类与抽象方法 (或 abstract 关键字)
# 6.1 由来
举例 1:
随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做 **抽象类**。
举例 2:
我们声明一些几何图形类:圆、矩形、三角形类等,发现这些类都有共同特征:求面积、求周长。那么这些共同特征应该抽取到一个共同父类:几何图形类中。但是这些方法在父类中又 无法给出具体的实现
,而是应该交给子类各自具体实现。那么父类在声明这些方法时, 就只有方法签名,没有方法体
,我们把没有方法体的方法称为抽象方法。Java 语法规定,包含抽象方法的类必须是抽象类。
抽象的必要性:多态。
在编写方法时,可以传入抽象类作形参,而实参可以是继承该抽象类的多种子类,减少了代码的冗余。
小结:都用关键字 abstract
修饰
抽象类
不能实例化对象
必须被继承,才能被使用
一个类只能继承一个(抽象)类,而一个类却可以实现多个接口
抽象方法
只有方法名,而没有方法体,方法的具体实现由它的子类各自确定
没有定义,方法名后面直接跟一个
;
,而不是{}
。
# 6.2 语法格式
- 抽象类:被 abstract 修饰的类。
- 抽象方法:被 abstract 修饰没有方法体的方法。
抽象类的语法格式
[权限修饰符] abstract class 类名{ | |
} | |
[权限修饰符] abstract class 类名 extends 父类{ | |
} |
抽象方法的语法格式
[其他修饰符] abstract 返回值类型 方法名([形参列表]); |
注意:抽象方法没有方法体
代码举例:
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(); | |
} | |
} |
此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法。
# 6.3 使用说明
抽象类不能创建对象,如果创建,编译无法通过而报错。只有抽象类的非抽象子类可以创建对象。
理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
- 抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。
- 若没有重写全部的抽象方法,仍为抽象类。
抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。
理解:子类的构造方法中,有默认的 super () 或手动的 super (实参列表),需要访问父类构造方法。
抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。
抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。
理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
# 6.4 注意事项
不能用 abstract 修饰变量、代码块、构造器;
不能用 abstract 修饰私有方法、静态方法、final 的方法、final 的类。
自洽:
- 私有方法不能被子类 override,而 abstract 方法必须要被子类实现,矛盾!
- 静态方法可以通过类名直接调用,而 abstract 方法是没有方法体的,不能被调用,矛盾!
- final 方法不能被子类 override,同 1
- final 类不能被继承,同 1
# 6.5 应用举例 1
在航运公司系统中,Vehicle 类需要定义两个方法分别 计算运输工具的燃料效率
和 行驶距离
。
** 问题:** 卡车 (Truck) 和驳船 (RiverBarge) 的燃料效率和行驶距离的计算方法完全不同。Vehicle 类不能提供计算方法,但子类可以。
** 解决方案:**Java 允许类设计者指定:超类声明一个方法但不提供实现,该方法的实现由子类提供。这样的方法称为抽象方法。有一个或更多抽象方法的类称为抽象类。
//Vehicle 是一个抽象类,有两个抽象方法。 | |
public abstract class Vehicle{ | |
public abstract double calcFuelEfficiency(); // 计算燃料效率的抽象方法 | |
public abstract double calcTripDistance(); // 计算行驶距离的抽象方法 | |
} | |
public class Truck extends Vehicle{ | |
public double calcFuelEfficiency( ) { // 写出计算卡车的燃料效率的具体方法 } | |
public double calcTripDistance( ) { // 写出计算卡车行驶距离的具体方法 } | |
} | |
public class RiverBarge extends Vehicle{ | |
public double calcFuelEfficiency( ) { // 写出计算驳船的燃料效率的具体方法 } | |
public double calcTripDistance( ) { // 写出计算驳船行驶距离的具体方法} | |
} |
# 6.6 应用举例 2:模板方法设计模式 (TemplateMethod)
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
解决的问题:
当功能内部一部分实现是确定的,另一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。
换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。
** 类比举例:** 英语六级模板
制作月饼的模板:
举例 1:
abstract class Template { | |
public final void getTime() { | |
long start = System.currentTimeMillis(); | |
code(); | |
long end = System.currentTimeMillis(); | |
System.out.println("执行时间是:" + (end - start)); | |
} | |
public abstract void code(); | |
} | |
class SubTemplate extends Template { | |
public void code() { | |
for (int i = 0; i < 10000; i++) { | |
System.out.println(i); | |
} | |
} | |
} |
举例 2:
package com.atguigu.java; | |
// 抽象类的应用:模板方法的设计模式 | |
public class TemplateMethodTest { | |
public static void main(String[] args) { | |
BankTemplateMethod btm = new DrawMoney(); | |
btm.process(); | |
BankTemplateMethod btm2 = new ManageMoney(); | |
btm2.process(); | |
} | |
} | |
abstract class BankTemplateMethod { | |
// 具体方法 | |
public void takeNumber() { | |
System.out.println("取号排队"); | |
} | |
public abstract void transact(); // 办理具体的业务 // 钩子方法 | |
public void evaluate() { | |
System.out.println("反馈评分"); | |
} | |
// 模板方法,把基本操作组合到一起,子类一般不能重写 | |
public final void process() { | |
this.takeNumber(); | |
this.transact();// 像个钩子,具体执行时,挂哪个子类,就执行哪个子类的实现代码 | |
this.evaluate(); | |
} | |
} | |
class DrawMoney extends BankTemplateMethod { | |
public void transact() { | |
System.out.println("我要取款!!!"); | |
} | |
} | |
class ManageMoney extends BankTemplateMethod { | |
public void transact() { | |
System.out.println("我要理财!我这里有2000万美元!!"); | |
} | |
} |
模板方法设计模式是编程中经常用得到的模式。各个框架、类库中都有他的影子,比如常见的有:
数据库访问的封装
Junit 单元测试
JavaWeb 的 Servlet 中关于 doGet/doPost 方法调用
Hibernate 中模板程序
Spring 中 JDBCTemlate、HibernateTemplate 等
# 6.7 思考与练习
思考:
问题 1:为什么抽象类不可以使用 final 关键字声明?
问题 2:一个抽象类中可以定义构造器吗?
问题 3:是否可以这样理解:抽象类就是比普通类多定义了抽象方法,除了不能直接进行类的实例化操作之外,并没有任何的不同?
练习 1:
编写一个 Employee 类,声明为抽象类,包含如下三个属性:name,id,salary。提供必要的构造器和抽象方法:work ()。
对于 Manager 类来说,他既是员工,还具有奖金 (bonus) 的属性。
请使用继承的思想,设计 CommonEmployee 类和 Manager 类,要求类中提供必要的方法进行属性访问。
练习 2:软件外包公司外派管理
有一家软件外包公司,可以外派开发人员,该公司有两个角色:普通开发人员 Developer 和项目经理 Manager。他们的关系如下图:
普通开发人员的工作内容是 “开发项目”,项目经理的工作内容是 “项目管理”。对外的报价是普通开发人员每天 500, 元,超过 60 天每天 400 元。项目经理每天 800 元,超过 60 天每天 700 元。
有一家银行需要 1 名项目经理、2 名开发人员,现场开发 90 天,计算银行需要付给软件公司的总金额。
提示:创建数组 Employee [] emps = new Employee [3]。其中存储驻场的 3 名员工。
练习 3:
创建父类 Shape,包含绘制形状的抽象方法 draw ()。
创建 Shape 的子类 Circle 和 Rectangle,重写 draw () 方法,绘制圆形和矩形。
绘制多个圆形和矩形。
练习 4:
1、声明抽象父类 Person,包含抽象方法 public abstract void eat ();
2、声明子类中国人 Chinese,重写抽象方法,打印用筷子吃饭
3、声明子类美国人 American,重写抽象方法,打印用刀叉吃饭
4、声明子类印度人 Indian,重写抽象方法,打印用手抓饭
5、声明测试类 PersonTest,创建 Person 数组,存储各国人对象,并遍历数组,调用 eat () 方法
练习 5:工资系统设计
编写工资系统,实现不同类型员工 (多态) 的按月发放工资。如果当月出现某个 Employee 对象的生日,则将该雇员的工资增加 100 元。
实验说明:
(1)定义一个 Employee 类,该类包含:
private 成员变量 name,number,birthday,其中 birthday 为 MyDate 类的对象;
abstract 方法 earnings ();
toString () 方法输出对象的 name,number 和 birthday。
(2)MyDate 类包含:
private 成员变量 year,month,day ;
toDateString () 方法返回日期对应的字符串:xxxx 年 xx 月 xx 日
(3)定义 SalariedEmployee 类继承 Employee 类,实现按月计算工资的员工处理。该类包括:private 成员变量 monthlySalary;
实现父类的抽象方法 earnings (), 该方法返回 monthlySalary 值;toString () 方法输出员工类型信息及员工的 name,number,birthday。
(4)参照 SalariedEmployee 类定义 HourlyEmployee 类,实现按小时计算工资的员工处理。该类包括:
private 成员变量 wage 和 hour;
实现父类的抽象方法 earnings (), 该方法返回 wage*hour 值;
toString () 方法输出员工类型信息及员工的 name,number,birthday。
(5)定义 PayrollSystem 类,创建 Employee 变量数组并初始化,该数组存放各类雇员对象的引用。利用循环结构遍历数组元素,输出各个对象的类型,name,number,birthday, 以及该对象生日。当键盘输入本月月份值时,如果本月是某个 Employee 对象的生日,还要输出增加工资信息。
// 提示: | |
// 定义 People 类型的数组 People c1 []=new People [10]; | |
// 数组元素赋值 | |
c1[0]=new People("John","0001",20); | |
c1[1]=new People("Bob","0002",19); | |
// 若 People 有两个子类 Student 和 Officer,则数组元素赋值时,可以使父类类型的数组元素指向子类。 | |
c1[0]=new Student("John","0001",20,85.0); | |
c1[1]=new Officer("Bob","0002",19,90.5); |
# 7. 接口 (interface)
# 7.1 类比
生活中大家每天都在用 USB 接口,那么 USB 接口与我们今天要学习的接口有什么相同点呢?
USB,(Universal Serial Bus,通用串行总线)是Intel公司开发的总线架构,使得在计算机上添加串行设备(鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3机、手机、数码相机、移动硬盘等)非常容易。
其实,不管是电脑上的 USB 插口,还是其他设备上的 USB 插口都只是 遵循了USB规范
的一种具体设备而已。
只要设备遵循 USB 规范的,那么就可以与电脑互联,并正常通信。至于这个设备、电脑是哪个厂家制造的,内部是如何实现的,我们都无需关心。
Java 的软件系统会有很多模块组成,那么各个模块之间也应该采用这种 面向接口
的 低耦合
,为系统提供更好的可扩展性和可维护性。
# 7.2 概述
接口就是规范,定义的是一组规则,体现了现实世界中 “如果你是 / 要... 则必须能...” 的思想。继承是一个 "是不是" 的 is-a 关系,而接口实现则是 "能不能" 的 has-a
关系。
- 例如:电脑都预留了可以插入 USB 设备的 USB 接口,USB 接口具备基本的数据传输的开启功能和关闭功能。你能不能用 USB 进行连接,或是否具备 USB 通信功能,就看你能否遵循 USB 接口规范
- 例如:Java 程序是否能够连接使用某种数据库产品,那么要看该数据库产品能否实现 Java 设计的 JDBC 规范
接口的本质是契约、标准、规范,就像我们的法律一样。制定好后大家都要遵守。
# 7.3 定义格式
接口的定义,它与定义类方式相似,但是使用 interface
关键字,它也会被编译成.class 文件,但一定要明确它并不是类,而是另外一种引用数据类型。
引用数据类型:数组,类,枚举,接口,注解。
# 7.3.1 接口的声明格式
[修饰符] interface 接口名{ | |
// 接口的成员列表: | |
// 公共的静态常量 | |
// 公共的抽象方法 | |
// 公共的默认方法(JDK1.8 以上) | |
// 公共的静态方法(JDK1.8 以上) | |
// 私有方法(JDK1.9 以上) | |
} |
示例代码:
package com.atguigu.interfacetype; | |
public interface USB3{ | |
// 静态常量 | |
long MAX_SPEED = 500*1024*1024;//500MB/s | |
// 抽象方法 | |
void in(); | |
void out(); | |
// 默认方法 | |
default void start(){ | |
System.out.println("开始"); | |
} | |
default void stop(){ | |
System.out.println("结束"); | |
} | |
// 静态方法 | |
static void show(){ | |
System.out.println("USB 3.0可以同步全速地进行读写操作"); | |
} | |
} |
# 7.3.2 接口的成员说明
在 JDK8.0 之前,接口中只允许出现:
(1)公共静态常量:其中 public static final
可以省略
(2)公共抽象方法:其中 public abstract
可以省略
理解:接口是从多个相似类中抽象出来的规范,不需要提供具体实现
在 JDK8.0 时,接口中允许声明 默认方法
和 静态方法
:
(3)公共默认方法:其中 public 可以省略,建议保留,但是default 不能省略
(4)公共静态方法:其中 public 可以省略,建议保留,但是static 不能省略
在 JDK9.0 时,接口又增加了:
(5)私有方法
除此之外,接口中没有构造器、初始化块
因为接口中没有成员变量需要动态初始化。
# 7.4 接口的使用规则
# 7.4.1 类实现接口
接口不能创建对象,但是可以被类实现( implements
,类似于被继承)。
类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements
关键字。
【修饰符】 class 实现类 implements 接口{ | |
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写 | |
// 重写接口中默认方法【可选】 | |
} | |
【修饰符】 class 实现类 extends 父类 implements 接口{ | |
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写 | |
// 重写接口中默认方法【可选】 | |
} |
注意:
如果接口的实现类是非抽象类,那么必须
重写接口中所有抽象方法
。默认方法可以选择保留,也可以重写。
重写时,default 单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了
接口中的静态方法不能被继承也不能被重写
举例:
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{ | |
public void start(){ // 重写方法 | |
System.out.println("U盘开始工作。") ; | |
} | |
public void stop(){ // 重写方法 | |
System.out.println("U盘停止工作。") ; | |
} | |
}; | |
class Print implements USB{ | |
public void start(){ // 重写方法 | |
System.out.println("打印机开始工作。") ; | |
} | |
public void stop(){ // 重写方法 | |
System.out.println("打印机停止工作。") ; | |
} | |
}; | |
public class InterfaceDemo{ | |
public static void main(String args[]){ | |
Computer.show(new Flash()) ; | |
Computer.show(new Print()) ; | |
c.show(new USB(){ | |
public void start(){ | |
System.out.println("移动硬盘开始运行"); | |
} | |
public void stop(){ | |
System.out.println("移动硬盘停止运行"); | |
} | |
}); | |
} | |
}; |
# 7.4.2 接口的多实现
之前学过,在继承体系中,一个类只能继承一个(直接)父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的 多实现
。并且,一个类能继承一个父类,同时实现多个接口。
实现格式:
【修饰符】 class 实现类 implements 接口1,接口2,接口3。。。{ | |
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写 | |
// 重写接口中默认方法【可选】 | |
} | |
【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{ | |
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写 | |
// 重写接口中默认方法【可选】 | |
} |
接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。
举例:
定义多个接口:
package com.atguigu.interfacetype; | |
public interface A { | |
void showA(); | |
} |
package com.atguigu.interfacetype; | |
public interface B { | |
void showB(); | |
} |
定义实现类:
package com.atguigu.interfacetype; | |
public class C implements A,B { | |
@Override | |
public void showA() { | |
System.out.println("showA"); | |
} | |
@Override | |
public void showB() { | |
System.out.println("showB"); | |
} | |
} |
测试类
package com.atguigu.interfacetype; | |
public class TestC { | |
public static void main(String[] args) { | |
C c = new C(); | |
c.showA(); | |
c.showB(); | |
} | |
} |
# 7.4.3 接口的多继承
一个接口能继承另一个或者多个接口,接口的继承也使用 extends
关键字,子接口继承父接口的方法。
定义父接口:
package com.atguigu.interfacetype; | |
public interface Chargeable { | |
void charge(); | |
void in(); | |
void out(); | |
} |
定义子接口:
package com.atguigu.interfacetype; | |
public interface UsbC extends Chargeable,USB3 { | |
void reverse(); | |
} |
定义子接口的实现类:
package com.atguigu.interfacetype; | |
public class TypeCConverter implements UsbC { | |
@Override | |
public void reverse() { | |
System.out.println("正反面都支持"); | |
} | |
@Override | |
public void charge() { | |
System.out.println("可充电"); | |
} | |
@Override | |
public void in() { | |
System.out.println("接收数据"); | |
} | |
@Override | |
public void out() { | |
System.out.println("输出数据"); | |
} | |
} |
所有父接口的抽象方法都要重写。
方法签名相同的抽象方法只需要实现一次。
# 7.4.4 接口与实现类对象构成多态引用
- 类的多态性:
父类 变量名 = new 子类对象;
- 接口的多态性:
接口名 变量名 = new 实现类对象;
实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。通过接口类型的变量调用方法,最终执行的是你 new 的实现类对象实现的方法体。
接口的不同实现类:
package com.atguigu.interfacetype; | |
public class Mouse implements USB3 { | |
@Override | |
public void out() { | |
System.out.println("发送脉冲信号"); | |
} | |
@Override | |
public void in() { | |
System.out.println("不接收信号"); | |
} | |
} |
package com.atguigu.interfacetype; | |
public class KeyBoard implements USB3{ | |
@Override | |
public void in() { | |
System.out.println("不接收信号"); | |
} | |
@Override | |
public void out() { | |
System.out.println("发送按键信号"); | |
} | |
} |
测试类
package com.atguigu.interfacetype; | |
public class TestComputer { | |
public static void main(String[] args) { | |
Computer computer = new Computer(); | |
USB3 usb = new Mouse(); | |
computer.setUsb(usb); | |
usb.start(); | |
usb.out(); | |
usb.in(); | |
usb.stop(); | |
System.out.println("--------------------------"); | |
usb = new KeyBoard(); | |
computer.setUsb(usb); | |
usb.start(); | |
usb.out(); | |
usb.in(); | |
usb.stop(); | |
System.out.println("--------------------------"); | |
usb = new MobileHDD(); | |
computer.setUsb(usb); | |
usb.start(); | |
usb.out(); | |
usb.in(); | |
usb.stop(); | |
} | |
} |
# 7.4.5 使用接口的静态成员
接口不能直接创建对象,但是可以通过接口名直接调用接口的静态常量、静态方法。
其中静态方法的调用是从 JDK8.0 才开始允许的
package com.atguigu.interfacetype; | |
public class TestUSB3 { | |
public static void main(String[] args) { | |
// 通过 “接口名.” 调用接口的静态方法 (JDK8.0 才能开始使用) | |
USB3.show(); | |
// 通过 “接口名.” 直接使用接口的静态常量 | |
System.out.println(USB3.MAX_SPEED); | |
} | |
} |
# 7.4.6 使用接口的非静态方法
- 对于接口的静态方法,直接使用 “
接口名.
” 进行调用即可- 也只能使用 “接口名." 进行调用,不能通过实现类的对象进行调用
- 对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用
- 接口不能直接创建对象,只能创建实现类的对象
package com.atguigu.interfacetype; | |
public class TestMobileHDD { | |
public static void main(String[] args) { | |
// 创建实现类对象 | |
MobileHDD b = new MobileHDD(); | |
// 通过实现类对象调用重写的抽象方法,以及接口的默认方法,如果实现类重写了就执行重写的默认方法,如果没有重写,就执行接口中的默认方法 | |
b.start(); | |
b.in(); | |
b.stop(); | |
// 通过接口名调用接口的静态方法 | |
// MobileHDD.show(); | |
// b.show(); | |
Usb3.show(); | |
} | |
} |
# 7.5 JDK8 中相关冲突问题
# 7.5.1 默认方法冲突问题
# (1)类优先原则
当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。代码如下:
定义接口:
package com.atguigu.interfacetype; | |
public interface Friend { | |
default void date(){// 约会 | |
System.out.println("吃喝玩乐"); | |
} | |
} |
定义父类:
package com.atguigu.interfacetype; | |
public class Father { | |
public void date(){// 约会 | |
System.out.println("爸爸约吃饭"); | |
} | |
} |
定义子类:
package com.atguigu.interfacetype; | |
public class Son extends Father implements Friend { | |
@Override | |
public void date() { | |
//(1) 不重写默认保留父类的 | |
//(2) 调用父类被重写的 | |
// super.date(); | |
//(3) 保留父接口的 | |
// Friend.super.date(); | |
//(4) 完全重写 | |
System.out.println("跟康师傅学Java"); | |
} | |
} |
定义测试类:
package com.atguigu.interfacetype; | |
public class TestSon { | |
public static void main(String[] args) { | |
Son s = new Son(); | |
s.date(); | |
} | |
} |
# (2)接口冲突(左右为难)
- 当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?
无论你多难抉择,最终都是要做出选择的。
声明接口:
package com.atguigu.interfacetype; | |
public interface BoyFriend { | |
default void date(){// 约会 | |
System.out.println("神秘约会"); | |
} | |
} |
通过 “ 接口名.super.方法名
" 的方法选择保留其中一个接口的默认方法。
package com.atguigu.interfacetype; | |
public class Girl implements Friend,BoyFriend{ | |
@Override | |
public void date() { | |
//(1) 保留其中一个父接口的 | |
// Friend.super.date(); | |
// BoyFriend.super.date(); | |
//(2) 完全重写 | |
System.out.println("跟康师傅学Java"); | |
} | |
} |
测试类
package com.atguigu.interfacetype; | |
public class TestGirl { | |
public static void main(String[] args) { | |
Girl girl = new Girl(); | |
girl.date(); | |
} | |
} |
- 当一个子接口同时继承了多个接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?
另一个父接口:
package com.atguigu.interfacetype; | |
public interface USB2 { | |
// 静态常量 | |
long MAX_SPEED = 60*1024*1024;//60MB/s | |
// 抽象方法 | |
void in(); | |
void out(); | |
// 默认方法 | |
public default void start(){ | |
System.out.println("开始"); | |
} | |
public default void stop(){ | |
System.out.println("结束"); | |
} | |
// 静态方法 | |
public static void show(){ | |
System.out.println("USB 2.0可以高速地进行读写操作"); | |
} | |
} |
子接口:
package com.atguigu.interfacetype; | |
public interface USB extends USB2,USB3 { | |
@Override | |
default void start() { | |
System.out.println("Usb.start"); | |
} | |
@Override | |
default void stop() { | |
System.out.println("Usb.stop"); | |
} | |
} |
小贴士:
子接口重写默认方法时,default 关键字可以保留。
子类重写默认方法时,default 关键字不可以保留。
# 7.5.2 常量冲突问题
- 当子类继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,并且该成员变量名在子类中仍然可见。
- 当子类同时实现多个接口,而多个接口存在相同同名常量。
此时在子类中想要引用父类或父接口的同名的常量或成员变量时,就会有冲突问题。
父类和父接口:
package com.atguigu.interfacetype; | |
public class SuperClass { | |
int x = 1; | |
} |
package com.atguigu.interfacetype; | |
public interface SuperInterface { | |
int x = 2; | |
int y = 2; | |
} |
package com.atguigu.interfacetype; | |
public interface MotherInterface { | |
int x = 3; | |
} |
子类:
package com.atguigu.interfacetype; | |
public class SubClass extends SuperClass implements SuperInterface,MotherInterface { | |
public void method(){ | |
// System.out.println ("x =" + x);// 模糊不清 | |
System.out.println("super.x = " + super.x); | |
System.out.println("SuperInterface.x = " + SuperInterface.x); | |
System.out.println("MotherInterface.x = " + MotherInterface.x); | |
System.out.println("y = " + y);// 没有重名问题,可以直接访问 | |
} | |
} |
# 7.6 接口的总结与面试题
- 接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。
- 声明接口用
interface
,接口的成员声明有限制:- (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
例如:尚硅谷学生行为规范中规定学员,早上 8:25 之前进班,晚上 21:30 之后离开等等。
2、为什么 JDK8.0 之后允许接口定义静态方法和默认方法呢?因为它违反了接口作为一个抽象标准定义的概念。
静态方法
:因为之前的标准类库设计中,有很多 Collection/Colletions 或者 Path/Paths 这样成对的接口和类,后面的类中都是静态方法,而这些静态方法都是为前面的接口服务的,那么这样设计一对 API,不如把静态方法直接定义到接口中使用和维护更方便。
默认方法
:(1)我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会涉及到原来使用这些接口的类就会有问题,那么为了保持与旧版本代码的兼容性,只能允许在接口中定义默认方法实现。比如:Java8 中对 Collection、List、Comparator 等接口提供了丰富的默认方法。(2)当我们接口的某个抽象方法,在很多实现类中的实现代码是一样的,此时将这个抽象方法设计为默认方法更为合适,那么实现类就可以选择重写,也可以选择不重写。
3、为什么 JDK1.9 要允许接口定义私有方法呢?因为我们说接口是规范,规范是需要公开让大家遵守的。
私有方法:因为有了默认方法和静态方法这样具有具体实现的方法,那么就可能出现多个方法有共同的代码可以抽取,而这些共同的代码抽取出来的方法又只希望在接口内部使用,所以就增加了私有方法。
# 7.7 接口与抽象类之间的对比
- 共性
- 都可以声明抽象方法
- 都不能实例化
- 不同
- 抽象类一定有构造器,而接口没有
- 类与类之间是单继承关系,类与接口之间是实现关系,接口与接口之间是多继承关系
在开发中,常看到一个类不是去继承一个已经实现好的类,而是要么继承抽象类,要么实现接口。
# 7.8 练习
** 笔试题:** 排错
interface A { | |
int x = 0; | |
} | |
class B { | |
int x = 1; | |
} | |
class C extends B implements A { | |
public void pX() { | |
System.out.println(x); | |
} | |
public static void main(String[] args) { | |
new C().pX(); | |
} | |
} |
** 笔试题:** 排错
interface Playable { | |
void play(); | |
} | |
interface Bounceable { | |
void play(); | |
} | |
interface Rollable extends Playable, Bounceable { | |
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() { | |
ball = new Ball("Football"); | |
System.out.println(ball.getName()); | |
} | |
} |
练习 1:
定义一个接口用来实现两个对象的比较。
interface CompareObject{ | |
// 若返回值是 0 , 代表相等;若为正数,代表当前对象大;负数代表当前对象小 | |
public int compareTo(Object o); | |
} |
定义一个 Circle 类,声明 redius 属性,提供 getter 和 setter 方法
定义一个 ComparableCircle 类,继承 Circle 类并且实现 CompareObject 接口。在 ComparableCircle 类中给出接口中方法 compareTo 的实现体,用来比较两个圆的半径大小。
定义一个测试类 InterfaceTest,创建两个 ComparableCircle 对象,调用 compareTo 方法比较两个类的半径大小。
思考:参照上述做法定义矩形类 Rectangle 和 ComparableRectangle 类,在 ComparableRectangle 类中给出 compareTo 方法的实现,比较两个矩形的面积大小。
练习 2:交通工具案例
阿里的一个工程师,声明的属性和方法如下:
其中,有一个乘坐交通工具的方法 takingVehicle (),在此方法中调用交通工具的 run ()。为了出行方便,他买了一辆捷安特自行车、一辆雅迪电动车和一辆奔驰轿车。这里涉及到的相关类及接口关系如下:
其中,电动车增加动力的方式是充电,轿车增加动力的方式是加油。在具体交通工具的 run () 中调用其所在类的相关属性信息。
请编写相关代码,并测试。
提示:创建 Vehicle [] 数组,保存阿里工程师的三辆交通工具,并分别在工程师的 takingVehicle () 中调用。
# 8. 类的成员之五:内部类(InnerClass)
面向对象的三条主线:
- 类及类的成员:属性、方法、构造器、代码块、内部类
- 三大特征:封装、继承、多态(、抽象)
- 其他关键字:this、super、package、import、static、final、interface、abstract、InnerClass 等
# 8.1 概述
# 8.1.1 什么是内部类
将一个类 A 定义在另一个类 B 里面,里面的那个类 A 就称为 内部类(InnerClass)
,类 B 则称为 外部类(OuterClass)
。
# 8.1.2 为什么要声明内部类呢
具体来说,当一个事物 A 的内部,还有一个部分需要一个完整的结构 B 进行描述,而这个内部的完整的结构 B 又只为外部事物 A 提供服务,不在其他地方单独使用,那么整个内部的完整结构 B 最好使用内部类。
总的来说,遵循 高内聚、低耦合
的面向对象开发原则。
# 8.1.3 内部类的分类
根据内部类声明的位置(如同变量的分类),我们可以分为:
成员内部类
声明在外部类的里面
- 静态的
- 非静态的
局部内部类
声明在方法、构造器、代码块中
- 匿名
- 非匿名的
# 8.2 成员内部类
# 8.2.1 概述
如果成员内部类中不使用外部类的非静态成员,那么通常将内部类声明为静态内部类,否则声明为非静态内部类。
语法格式:
[修饰符] class 外部类{ | |
[其他修饰符] [static] class 内部类{ | |
} | |
} |
成员内部类的使用特征,概括来讲有如下两种角色:
- 成员内部类作为
类的成员的角色
:- 和外部类(只能使用 public、缺省权限修饰符)不同,Inner class 还可以声明为 private 或 protected;
- 可以调用外部类的结构。(注意:在静态内部类中不能使用外部类的非静态成员)
- 可以声明为 static 的,但此时就不能再使用外层类的非 static 的成员变量;
- 成员内部类作为
类的角色
:- 可以在内部定义属性、方法、构造器等结构
- 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
- 可以声明为 abstract 类 ,因此可以被其它的内部类继承
- 可以声明为 final 的,表示不能被继承
- 编译以后生成
OuterClass$InnerClass.class
字节码文件(也适用于局部内部类)
注意点:
外部类访问成员内部类的成员,需要 “内部类。成员” 或 “内部类对象。成员” 的方式
成员内部类可以直接使用外部类的所有成员,包括私有的数据
当想要在外部类的静态成员部分使用内部类时,可以考虑内部类声明为静态的
# 8.2.2 创建成员内部类对象
- 实例化静态内部类:
new 外部类名.静态内部类名()
外部类名.静态内部类名 变量 = new 外部类名.静态内部类名();
变量.非静态方法();
- 实例化非静态内部类:先创建外部类的实例对象,再调用该实例对象
.new 非静态内部类名()
外部类名 变量1 = new 外部类();
外部类名.非静态内部类名 变量2 = 变量1.new 非静态内部类名();
变量2.非静态方法();
# 8.2.3 举例
public class TestMemberInnerClass { | |
public static void main(String[] args) { | |
// 创建静态内部类实例,并调用方法 | |
Outer.StaticInner inner = new Outer.StaticInner(); | |
inner.inFun(); | |
// 调用静态内部类静态方法 | |
Outer.StaticInner.inMethod(); | |
System.out.println("*****************************"); | |
// 创建非静态内部类实例(方式 1),并调用方法 | |
Outer outer = new Outer(); | |
Outer.NoStaticInner inner1 = outer.new NoStaticInner(); | |
inner1.inFun(); | |
// 创建非静态内部类实例(方式 2) | |
Outer.NoStaticInner inner2 = outer.getNoStaticInner(); | |
inner1.inFun(); | |
} | |
} | |
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(); | |
} | |
} |
# 8.3 局部内部类
# 8.3.1 非匿名局部内部类
语法格式:
[修饰符] class 外部类{ | |
[修饰符] 返回值类型 方法名(形参列表){ | |
[final/abstract] class 内部类{ | |
} | |
} | |
} |
- 编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名、$ 符号、编号。
- 这里有编号是因为同一个外部类中,不同的方法中存在相同名称的局部内部类
- 和成员内部类不同的是,它前面不能有权限修饰符等
- 局部内部类如同局部变量一样,有作用域
- 局部内部类中是否能访问外部类的非静态的成员,取决于所在的方法
举例:
/** | |
* ClassName: TestLocalInner | |
* @Author 尚硅谷 - 宋红康 | |
* @Create 17:19 | |
* @Version 1.0 | |
*/ | |
public class TestLocalInner { | |
public static void main(String[] args) { | |
Outer.outMethod(); | |
System.out.println("-------------------"); | |
Outer out = new Outer(); | |
out.outTest(); | |
System.out.println("-------------------"); | |
Runner runner = Outer.getRunner(); | |
runner.run(); | |
} | |
} | |
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(); | |
} |
# 8.3.2 匿名内部类
因为考虑到这个子类或实现类是一次性的,那么我们 “费尽心机” 的给它取名字,就显得多余。那么我们完全可以使用匿名内部类的方式来实现,避免给类命名的问题。
new 父类([实参列表]){ | |
重写方法... | |
} |
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"); | |
} | |
}); | |
} | |
} |
# 8.4 练习
练习:判断输出结果为何?
public class Test { | |
public Test() { | |
Inner s1 = new Inner(); | |
s1.a = 10; | |
Inner s2 = new Inner(); | |
s2.a = 20; | |
Test.Inner s3 = new Test.Inner(); | |
System.out.println(s3.a); | |
} | |
class Inner { | |
public int a = 5; | |
} | |
public static void main(String[] args) { | |
Test t = new Test(); | |
Inner r = t.new Inner(); | |
System.out.println(r.a); | |
} | |
} |
练习 2:
编写一个匿名内部类,它继承 Object,并在匿名内部类中,声明一个方法 public void test () 打印尚硅谷。
请编写代码调用这个方法。
package com.atguigu.test01; | |
public class Test01 { | |
public static void main(String[] args) { | |
new Object(){ | |
public void test(){ | |
System.out.println("尚硅谷"); | |
} | |
}.test(); | |
} | |
} |
# 9. 枚举类(enum)
# 9.1 概述
枚举类型本质上也是一种类,只不过是这个类的对象是有限的、固定的几个,不能让用户随意创建。
枚举类的例子举不胜举:
星期
: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
关键字来快速定义枚举类型。
# 9.2 定义枚举类(JDK5.0 之前)
在 JDK5.0 之前如何声明枚举类呢?
私有化
类的构造器,保证不能在类的外部创建其对象- 在类的内部创建枚举类的实例。声明为:
public static final
,对外暴露这些常量对象 - 对象如果有
实例变量
,应该声明为private final
(建议,不是必须),并在构造器中初始化
示例代码:
class Season{ | |
// 2. 声明当前类的实例变量,用 private final 修饰 | |
private final String SEASONNAME;// 季节的名称 | |
private final String SEASONDESC;// 季节的描述 | |
// 1. 私有化类的构造器 | |
private Season(String seasonName,String seasonDesc){ | |
this.SEASONNAME = seasonName; | |
this.SEASONDESC = seasonDesc; | |
} | |
// 3. 创建当前类的实例对象,用 public static final 修饰 | |
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{" + | |
"SEASONNAME='" + SEASONNAME + '\'' + | |
", SEASONDESC='" + SEASONDESC + '\'' + | |
'}'; | |
} | |
} | |
class SeasonTest{ | |
public static void main(String[] args) { | |
System.out.println(Season.AUTUMN); | |
} | |
} |
# 9.3 定义枚举类(JDK5.0 之后)
# 9.3.1 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) { | |
Season spring = Season.SPRING; | |
System.out.println(spring); | |
} | |
} |
# 9.3.2 enum 方式定义的要求和特点
- 枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写。
- 列出的实例系统会自动添加
public static final
修饰。 - 如果常量对象列表后面没有其他代码,那么 “;” 可以省略,否则不可以省略 “;”。
- 编译器给枚举类默认提供的是 private 的无参构造,如果枚举类需要的是无参构造,就不需要声明,写常量对象列表时也不用加参数
- 如果枚举类需要的是有参构造,需要手动定义,有参构造的 private 可以省略,调用有参构造的方法就是在常量对象名后面加
(实参列表)
就可以。 - 枚举类默认继承的是
java.lang.Enum
类,因此不能再继承其他的类型。 - JDK5.0 之后 switch,提供支持枚举类型,case 后面可以写枚举常量名,无需添加枚举类作为限定。
举例 2:
public enum SeasonEnum { | |
// 1. 常量对象列表(括号是在调用有参构造器) | |
SPRING("春天","春风又绿江南岸"), | |
SUMMER("夏天","映日荷花别样红"), | |
AUTUMN("秋天","秋水共长天一色"), | |
WINTER("冬天","窗含西岭千秋雪"); | |
// 2. 实例变量列表 | |
private final String seasonName; | |
private final String seasonDesc; | |
// 3. 有参构造器 | |
private SeasonEnum(String seasonName, String seasonDesc) { | |
this.seasonName = seasonName; | |
this.seasonDesc = seasonDesc; | |
} | |
// 实例变量的 get 方法 | |
public String getSeasonName() { | |
return seasonName; | |
} | |
public String getSeasonDesc() { | |
return seasonDesc; | |
} | |
} |
举例 3:
package com.atguigu.enumeration; | |
public enum Week { | |
MONDAY("星期一"), | |
TUESDAY("星期二"), | |
WEDNESDAY("星期三"), | |
THURSDAY("星期四"), | |
FRIDAY("星期五"), | |
SATURDAY("星期六"), | |
SUNDAY("星期日"); | |
private final String description; | |
private Week(String description){ | |
this.description = description; | |
} | |
@Override | |
public String toString() { | |
return super.toString() +":"+ description; | |
} | |
} |
package com.atguigu.enumeration; | |
public class TestWeek { | |
public static void main(String[] args) { | |
Week week = Week.MONDAY; | |
System.out.println(week); | |
switch (week){ | |
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; | |
} | |
} | |
} |
经验之谈:
开发中,当需要定义一组常量时,强烈建议使用枚举类。
# 9.4 enum 中常用方法
- String
toString()
:默认返回的是常量名(对象名),可以继续手动重写该方法! - String
name()
:得到当前枚举常量的名称。建议优先使用toString()
。 - static 枚举类型 []
values()
:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值,是一个静态方法 - static 枚举类型
valueOf(String name)
:可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的 “名字”。如不是,会有运行时异常:IllegalArgumentException。 - int
ordinal()
:返回当前枚举常量的次序号,默认从 0 开始
举例:
package com.atguigu.enumeration; | |
import java.util.Scanner; | |
public class TestEnumMethod { | |
public static void main(String[] args) { | |
//values() | |
Week[] values = Week.values(); | |
for (int i = 0; i < values.length; i++) { | |
//ordinal()、name() | |
System.out.println((values[i].ordinal()+1) + "->" + values[i].name()); | |
} | |
System.out.println("------------------------"); | |
Scanner input = new Scanner(System.in); | |
System.out.print("请输入星期值:"); | |
int weekValue = input.nextInt(); | |
Week week = values[weekValue-1]; | |
//toString() | |
System.out.println(week); | |
System.out.print("请输入星期名:"); | |
String weekName = input.next(); | |
//valueOf() | |
week = Week.valueOf(weekName); | |
System.out.println(week); | |
input.close(); | |
} | |
} |
# 9.5 实现接口的枚举类
- 和普通 Java 类一样,枚举类可以实现一个或多个接口
- 若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。
- 若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法
语法:
//1、枚举类可以像普通的类一样,实现接口,并且可以多个,但要求必须实现里面所有的抽象方法! | |
enum A implements 接口1,接口2{ | |
// 抽象方法的实现 | |
} | |
//2、如果枚举类的常量可以继续重写抽象方法! | |
enum A implements 接口1,接口2{ | |
常量名1(参数){ | |
// 抽象方法的实现或重写 | |
}, | |
常量名2(参数){ | |
// 抽象方法的实现或重写 | |
}, | |
//... | |
} |
举例:
interface Info{ | |
void show(); | |
} | |
// 使用 enum 关键字定义枚举类 | |
enum Season1 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 Season1(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; | |
} | |
} |
# 10. 注解 (Annotation)
# 10.1 注解概述
# 10.1.1 什么是注解
**注解(Annotation)** 是从 JDK5.0
开始引入,以 “ @注解名
” 在代码中存在。例如:
@Override |
@Deprecated |
@SuppressWarnings(value=”unchecked”) |
Annotation 可以像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明。还可以添加一些参数值,这些信息被保存在 Annotation 的 “name=value” 对中。
注解可以在类编译、运行时进行加载,体现不同的功能。
# 10.1.2 注解与注释
注解也可以看做是一种注释,通过使用 Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。但是,注解,不同于单行注释和多行注释。
- 对于单行注释和多行注释是给程序员看的。
- 而注解是可以被编译器或其他程序读取的。程序还可以根据注解的不同,做出相应的处理。
# 10.1.3 注解的重要性
在 JavaSE 中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在 JavaEE/Android中注解占据了更重要的角色
,例如用来配置应用程序的任何切面,代替 JavaEE 旧版中所遗留的 繁冗代码
和 XML配置
等。
未来的开发模式都是基于注解的,JPA 是基于注解的,Spring2.5 以上都是基于注解的,Hibernate3.x 以后也是基于注解的,Struts2 有一部分也是基于注解的了。 注解是一种趋势
,一定程度上可以说: 框架 = 注解 + 反射 + 设计模式
。
反射:调取注解
# 10.2 常见的 Annotation 作用
示例 1:生成文档相关的注解
@author 标明开发该类模块的作者,多个作者之间使用,分割 | |
@version 标明该类模块的版本 | |
@see 参考转向,也就是相关主题 | |
@since 从哪个版本开始增加的 | |
@param 对方法中某参数的说明,如果没有参数就不能写 | |
@return 对方法返回值的说明,如果方法的返回值类型是void就不能写 | |
@exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写 |
package com.annotation.javadoc; | |
/** | |
* @author 尚硅谷 - 宋红康 | |
* @version 1.0 | |
* @see Math.java | |
*/ | |
public class JavadocTest { | |
/** | |
* 程序的主方法,程序的入口 | |
* @param args String [] 命令行参数 | |
*/ | |
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
: 抑制编译器警告
package com.annotation.javadoc; | |
public class AnnotationTest{ | |
public static void main(String[] args) { | |
@SuppressWarnings("unused") | |
int a = 10; | |
} | |
@Deprecated | |
public void print(){ | |
System.out.println("过时的方法"); | |
} | |
@Override | |
public String toString() { | |
return "重写的toString方法()"; | |
} | |
} |
示例 3:跟踪代码依赖性,实现替代配置文件功能
- Servlet3.0 提供了注解 (annotation),使得不再需要在 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> |
# 10.3 三个最基本的注解
# 10.3.1 @Override
用于检测被标记的方法为有效的重写方法,如果不是,则报编译错误!
只能标记在方法上。
它会被编译器程序读取。
# 10.3.2 @Deprecated
用于表示被标记的数据已经过时,不推荐使用。
可以用于修饰 属性、方法、构造、类、包、局部变量、参数。
它会被编译器程序读取。
# 10.3.3 @SuppressWarnings
抑制编译警告。当我们不希望看到警告信息的时候,可以使用 SuppressWarnings 注解来抑制警告信息
可以用于修饰类、属性、方法、构造、局部变量、参数
它会被编译器程序读取。
可以指定的警告类型有(了解)
- all,抑制所有警告
- unchecked,抑制与未检查的作业相关的警告
- unused,抑制与未用的程式码及停用的程式码相关的警告
- deprecation,抑制与淘汰的相关警告
- nls,抑制与非 nls 字串文字相关的警告
- null,抑制与空值分析相关的警告
- rawtypes,抑制与使用 raw 类型相关的警告
- static-access,抑制与静态存取不正确相关的警告
- static-method,抑制与可能宣告为 static 的方法相关的警告
- super,抑制与置换方法相关但不含 super 呼叫的警告
- ...
示例代码:
package com.atguigu.annotation; | |
import java.util.ArrayList; | |
public class TestAnnotation { | |
@SuppressWarnings("all") | |
public static void main(String[] args) { | |
int i; | |
ArrayList list = new ArrayList(); | |
list.add("hello"); | |
list.add(123); | |
list.add("world"); | |
Father f = new Son(); | |
f.show(); | |
f.methodOl(); | |
} | |
} | |
class Father{ | |
@Deprecated | |
void show() { | |
System.out.println("Father.show"); | |
} | |
void methodOl() { | |
System.out.println("Father Method"); | |
} | |
} | |
class Son extends Father{ | |
/* @Override | |
void method01() { | |
System.out.println("Son Method"); | |
}*/ | |
} |
# 10.4 元注解
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:** 描述注解是否被子类继承
示例代码:
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 { | |
} |
拓展:元数据
String name = "Tom";
# 10.5 自定义注解的使用
一个完整的注解应该包含三个部分:声明、使用、读取。
# 10.5.1 声明
【元注解】 | |
【修饰符】 @interface 注解名{ | |
【成员列表】 | |
} |
- 自定义注解可以通过四个元注解@Retention,@Target,@Inherited,@Documented,分别说明它的生命周期,使用位置,是否被继承,是否被生成到 API 文档中。
- Annotation 的成员在 Annotation 定义中以无参数有返回值的抽象方法的形式来声明,我们又称为配置参数。返回值类型只能是八种基本数据类型、String 类型、Class 类型、enum 类型、Annotation 类型、以上所有类型的数组
- 可以使用 default 关键字为抽象方法指定默认返回值
- 如果定义的注解含有抽象方法,那么使用时必须指定返回值,除非它有默认值。格式是 “方法名 = 返回值”,如果只有一个抽象方法需要赋值,且方法名为 value,可以省略 “value=”,所以如果注解只有一个抽象方法成员,建议使用方法名 value。
/* 元注解:用于描述注解的注解 | |
* @Target:描述注解能够作用的位置 | |
* ElementType 取值:(10 个) | |
* TYPE:可以作用于类上 | |
* METHOD:可以作用于方法上 | |
* FIELD:可以作用于成员变量上 | |
* PARAMETER:可以作用于参数上 | |
* CONSTRUCTOR:可以作用于构造器上 | |
* LOCAL_VARIABLE:可以作用于局部变量上 | |
* ANNOTATION_TYPE:可以作用于注解上 | |
* PACKAGE:可以作用于包上 | |
* TYPE_PARAMETER:可以作用于类型变量上 | |
* TYPE_USE:可以作用于类型上 | |
* @Retention:描述注解被保留的阶段(生命周期) | |
* RetentionPolicy 取值:(3 个) | |
* SOURCE:在源文件中有效(即源文件保留) | |
* CLASS:在 class 文件中有效(即 class 保留) | |
* RUNTIME:在运行时有效(即运行时保留) | |
* @Documented:描述注解是否被抽取到 api 文档中 | |
* @Inherited:描述注解是否被子类继承 | |
*/ | |
@Target(value = {ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE, ElementType.ANNOTATION_TYPE, ElementType.PACKAGE, ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) | |
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME) | |
@Documented | |
@Inherited | |
public @interface MyAnnotation { | |
// 注解的属性,只能是基本数据类型、String、Class、枚举、注解、以上类型的数组。 | |
// 如果注解只有一个属性,建议将属性名命名为 value,使用时可以省略属性名和赋值号(=),直接写属性值,例如:@MyAnnotation ("hello"), | |
// 如果注解有多个属性,使用时必须给属性赋值,否则编译报错 | |
String value(); | |
int age() default 18; // 注解的属性,可以有默认值,使用 default 关键字,如果没有默认值,使用注解时必须给属性赋值,否则编译报错 | |
String[] newNames(); // 注解的属性,可以是数组 | |
} |
package com.atguigu.annotation; | |
import java.lang.annotation.*; | |
@Inherited | |
@Target(ElementType.FIELD) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface Column { | |
String columnName(); | |
String columnType(); | |
} |
# 10.5.2 使用
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; | |
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 + '\'' + | |
'}'; | |
} | |
} |
# 10.5.3 读取和处理
自定义注解必须配上注解的信息处理流程才有意义。
我们自己定义的注解,只能使用反射的代码读取。此时生命周期必须是 RetentionPolicy.RUNTIME
。
具体的使用见 《尚硅谷_宋红康_第17章_反射机制.md》
。
# 10.6 JUnit 单元测试
# 10.6.1 测试分类
**黑盒测试:** 不需要写代码,给输入值,看程序是否能够输出期望的值。
功能测试
白盒测试:需要写代码的。关注程序具体的执行流程。
单元测试
# 10.6.2 JUnit 单元测试介绍
JUnit 是由 Erich Gamma 和 Kent Beck 编写的一个测试框架(regression testing framework),供 Java 开发人员编写单元测试之用。
JUnit 测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。
要使用 JUnit,必须在项目的编译路径中 引入JUnit的库
,即相关的.class 文件组成的 jar 包。
jar 就是一个压缩包,压缩包都是开发好的第三方(Oracle 公司第一方,我们自己第二方,其他都是第三方)工具类,都是以class 文件形式存在的。
# 10.6.3 引入本地 JUnit.jar
第 1 步:在项目中 File-Project Structure 中操作:添加 Libraries 库
其中,junit-libs 包内容如下:
第 2 步:选择要在哪些 module 中应用 JUnit 库
第 3 步:检查是否应用成功
注意 Scope:选择 Compile,否则编译时,无法使用 JUnit。
第 4 步:下次如果有新的模块要使用该 libs 库,这样操作即可
# 10.6.4 编写和运行 @Test 单元测试方法
JUnit4 版本,要求 @Test 标记的方法必须满足如下要求:
- 所在的类必须是 public 的,非抽象的,包含唯一的无参构造器。
- @Test 标记的方法本身必须是 public,非抽象的,非静态的,void 无返回值,() 无参数的。
package com.atguigu.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"); | |
} | |
} |
# 10.6.5 设置执行 JUnit 用例时支持控制台输入
1. 设置数据:
默认情况下,在单元测试方法中使用 Scanner 时,并不能实现控制台数据的输入。需要做如下设置:
在 idea64.exe.vmoptions配置文件
中加入下面一行设置,重启 idea 后生效。
-Deditable.java.test.console=true |
2. 配置文件位置:
添加完成之后,重启 IDEA 即可。
3. 如果上述位置设置不成功,需要继续修改如下位置
修改位置 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` 件。
# 10.6.6 定义 test 测试方法模板
选中自定义的模板组,点击”+”(1.Live Template)来定义模板。
# 11. 包装类
# 11.1 为什么需要包装类
Java 提供了两个类型系统, 基本数据类型
与 引用数据类型
。使用基本数据类型在于效率,然而当要使用只针对对象设计的 API 或新特性(例如泛型),怎么办呢?
为了使得基本数据类型的变量具备引用数据类型的相关特征(封装性、继承性、多态性等),给 8 个基本数据类型提供了对应的包装类。
// 情况 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> |
# 11.2 有哪些包装类
因此,包装类的作用就是将基本类型转成对象,将基本类型作为对象来处理。 Java 中我们知道,基本数据类型有 8 个,所以对应的包装类也是 8 个,包装类就是基本类型名称首字母大写, 但 Integer 和 Character 例外,它们显示全称,如下面表格所示:
Java 针对八种基本数据类型定义了相应的引用类型:包装类(封装类)。有了类的特点,就可以调用类中的方法,Java 才是真正的面向对象。
封装以后的,内存结构对比:
public static void main(String[] args){ | |
int num = 520; | |
Integer obj = new Integer(520); | |
} |
# 11.3 自定义包装类
public class MyInteger { | |
int value; | |
public MyInteger() { | |
} | |
public MyInteger(int value) { | |
this.value = value; | |
} | |
@Override | |
public String toString() { | |
return String.valueOf(value); | |
} | |
} |
# 11.4 包装类与基本数据类型间的转换
# 11.4.1 装箱
装箱:把基本数据类型转为包装类对象
- 构造函数
- 静态方法 valueOf ()
转为包装类的对象,是为了使用专门为对象设计的 API 和特性
基本数值 ----> 包装对象
Integer obj1 = new Integer(4);// 使用构造函数函数 | |
Float f = new Float(“4.56”); | |
Long l = new Long(“asdf”); //NumberFormatException | |
Integer obj2 = Integer.valueOf(4);// 使用包装类中的 valueOf 方法 |
# 11.4.2 拆箱
拆箱:把包装类对象拆为基本数据类型
- 实例方法 xxxValue ()
转为基本数据类型,一般是因为需要运算,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;// 错误的,1 是 int 类型 |
# 11.5 基本数据类型、包装类与字符串间的转换
(1)基本数据类型转为字符串
方式 1: String.valueOf(xxx)
int a = 10; | |
//String str = a;// 错误的 | |
String str = String.valueOf(a); |
** 方式 2:** 更直接的方式
int a = 10; | |
String str = a + ""; |
(2)字符串转为基本数据类型
** 方式 1:** 除了 Character 类之外,其他所有包装类都具有 包装类.parseXxx(str)
静态方法可以将字符串参数转换为对应的基本类型,例如:
public static int parseInt(String s)
:将字符串参数转换为对应的 int 基本类型。public static long parseLong(String s)
:将字符串参数转换为对应的 long 基本类型。public static double parseDouble(String s)
:将字符串参数转换为对应的 double 基本类型。
** 方式 2:** 通过 包装类.valueOf(str)
静态方法先将字符串转为包装类,然后可以自动拆箱为基本数据类型
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 i = new Integer(“12”); |
其他方式小结:
得益于
JDK5
开始提供的自动装箱、自动开箱,可以不再仔细区分下图中的 “基本数据类型” 和 “包装类”了。记忆要诀:当 source 要转换为 target 时,从 target 中寻找方法!
省流:
基本数据类型 / 包装类 ---> String 类
String.valueOf(xxx)
String 类 ---> 基本数据类型 / 包装类
包装类.parseXxx(str)
如果 str 的内容无法正确转换为对应的基本类型,可能会抛出异常
NumberFormatException
# 11.6 包装类的其它 API
# 11.6.1 数据类型的最大最小值
Integer.MAX_VALUE和Integer.MIN_VALUE | |
Long.MAX_VALUE和Long.MIN_VALUE | |
Double.MAX_VALUE和Double.MIN_VALUE |
# 11.6.2 字符转大小写
Character.toUpperCase('x'); | |
Character.toLowerCase('X'); |
# 11.6.3 整数转进制
Integer.toBinaryString(int i) | |
Integer.toHexString(int i) | |
Integer.toOctalString(int i) |
# 11.6.4 比较的方法
Double.compare(double d1, double d2) | |
Integer.compare(int x, int y) |
# 11.7 包装类对象的特点
# 11.7.1 包装类缓存对象
包装类 | 缓存对象 |
---|---|
Byte | -128~127 |
Short | -128~127 |
Integer | -128~127 |
Long | -128~127 |
Float | 没有 |
Double | 没有 |
Character | 0~127 |
Boolean | true 和 false |
对于整数型基本数据类型对应的包装类(Byte、Short、Integer、Long),缓存了常量池
-128 ~ 127
存放在数组中,在对包装类对象赋值时:
- 数值在
-128 ~ 127
内的,直接使用缓冲在方法区中的常量对象,此时判断==
是 true- 数值超出
-128 ~ 127
的,会新 new 一个包装类对象在堆空间中,此时判断==
是 false
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 的 |
# 11.7.2 类型转换问题
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);// 编译报错 |
# 11.7.3 包装类对象不可变
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; | |
} |
# 11.8 练习
笔试题:如下两个题目输出结果相同吗?各是什么。
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 的 valueOf () | |
Integer m = 1; // 缓冲的常量对象,在方法区 | |
Integer n = 1; // 缓冲的常量对象,在方法区 | |
System.out.println(m == n);// true | |
Integer x = 128; // 新 new 的在堆中 | |
Integer y = 128; // 新 new 的在堆中 | |
System.out.println(x == y);// false | |
Integer i1 = 10; | |
Double d1 = 10.2; | |
System.out.println(i1 == d1); // 编译报错,== 是可以比较引用数据类型的,但是要求两边的数据类型要么相同,要么有继承关系 | |
Integer m = 1000; | |
double n = 1000; | |
System.out.println(m == n); //true。Integer 会自动拆箱为 int,然后自动类型提升为 double,然后再进行比较 | |
Integer x = 1000; | |
int y = 1000; | |
System.out.println(x == y); // true | |
} |
练习:
利用 Vector 代替数组处理:从键盘读入学生成绩(以负数代表输入结束),找出最高分,并输出学生成绩等级。
提示:数组一旦创建,长度就固定不变,所以在创建数组前就需要知道它的长度。而向量类 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 等