1. Java 8 增强的包装类
1.1 基本数据类型的包装类
- 为了解决8种基本数据类型的变量不能当成 Object 类型变量使用的问题, 才出现了包装类(WrapperClass)的概念, 为这8种基本类型的数据分别定义了相应的引用类型
- 熟记下面的对应关系, 着重记住 int---Integer 和 char---Character 这两种比较特殊的对应关系
- 包装类的初始值为null
- 基本数据类型存储在栈(stack)中,而包装类分成引用和实例,引用存储在栈内存中,而具体的实例则存储在堆(heap)中
基本数据类型和引用数据类型
<基本数据类型> | <引用数据类型> |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
1.2 自动装箱和自动拆箱
-
自动装箱(Autoboxing)
: (基本类型和包装类之间的转换自动/手动)就是可以把一个基本类型变量直接赋给对应的包装类变量, 或者赋给 Object变量(Object 是所有类的父类, 子类对象可以直接赋给父类变量);装箱时调用valueOf()方法实现,比如:
自动装箱: Integer i = 100;
手动装箱: Integer i = Integer.valueOf(100); -
自动拆箱(AutoUnboxing)
: 把包装类对象直接赋给一个对应的基本类型变量; 拆箱时调用对应的XxxValue() 方法实现, 比如:intValue() 方法
自动拆箱比如: Integer i = 100;
int t1 = i; // 自动拆箱
手动拆箱比如: int t2 = i.intValue(); -
进行自动装箱和自动拆箱时必须注意类型匹配, 如: Integer 只能自动拆箱成 int 类型变量, 不能拆成别的类型; 同样, int 类型变量只能自动装箱成 Integer 对象 (例子1)
-
目的: 通过自动装箱和自动拆箱功能, 开发者可以把基本类型的变量 "近似" 地当成对象使用, 同样,也可以把包装类的实例近似地当成基本类型的变量使用
1.3 基本类型和字符串之间的转换(例子2)
基本类型转换为字符串:(3种方法)
- 通过String类的 String.valueOf(primitive) 方法
- 使用包装类的 toString()方法,WrapperClass.toString() 方法
- 使用基本类型加上一个空字符串方法: 基本类型 + ""
字符串转换为基本类型:(2种方法)
- 通过 WrapperClass.parseXxx() 方法
- 调用包装类的 WrapperClass.valueOf()方法,会自动拆箱
代码示例
// 例子1: 基本类型和包装类之间的互相转换
// 自动装箱和自动拆箱
public class AutoboxingUnboxing{
public static void main(String[] args){
// 直接把一个基本类型变量赋给 Integer 对象(自动装箱)
Integer inObj = 5;
// 直接把一个 boolean 类型的变量赋给一个 Object 类型的变量(自动装箱)
Object boolObj = true;
// 直接把一个 Integer 对象赋给 int 类型的变量(自动拆箱)
int it = inObj;
System.out.println(it); out: 5
// instanceof 方法用于测试左边的对象是否是它右边类的实例或子类, 返回的是 boolean 数据类型
if (boolObj instanceof Boolean){
// 先把 Object 对象强制类型转换为 Boolean 类型, 在赋给 boolean 变量
boolean b = (Boolean)boolObj;
System.out.println(b); // out: true
}
}
}
// 程序说明:
// 1. 基本类型转引用数据类型: 直接将基本类型的值赋值给引用数据类型的变量
// 2. 引用数据类型转基本类型: 直接将引用类型值赋值给基本数据类型的变量
// 例子2:
// 字符串和基本类型之间的转换
public class Primitive2String{
public static void main(String[] args){
String intStr = "123";
// 把一个特定的字符串转换为 int 变量 (2种方法)
int it1 = Integer.parseInt(intStr);
int it2 = Integer.valueOf(intStr);
System.out.println(it1); // out: 123
System.out.println(it2); // out: 123
String floatStr = "3.14";
// 把一个特定的字符串转换成 float 变量 (2种方法)
float ft1 = Float.parseFloat(floatStr);
float ft2 = Float.valueOf(floatStr);
System.out.println(ft1); // out: 3.14
System.out.println(ft2); // out: 3.14
// 把一个float 变量转换成 String 变量 (3种方法)
String ftStr1 = String.valueOf(3.1415926F);
String ftStr2 = Float.toString(3.1415926F);
String ftStr3 = 3.1415926F + "";
System.out.println(ftStr1); // out: 3.1415925
System.out.println(ftStr2); // out: 3.1415925
System.out.println(ftStr3); // out: 3.1415925
// 把一个 double 变量转换成 String 变量 (3种方法)
String dbStr1 = String.valueOf(6.25789);
String dbStr2 = Double.toString(6.257899);
String dbStr3 = 6.2578899 + "";
System.out.println(dbStr1); // out: 6.25789
System.out.println(dbStr2); // out: 6.257899
System.out.println(dbStr3); // out: 6.2578899
// 把一个 boolean 变量转换成 String 变量 (3种方法)
String boolStr1 = String.valueOf(true);
String boolStr2 = Boolean.toString(false);
String boolStr3 = true + "";
System.out.println(boolStr1.toUpperCase()); // out: TRUE
System.out.println(boolStr2.toUpperCase()); // out: FALSE
System.out.println(boolStr3.toUpperCase()); // out: TRUE
}
}
1.4 包装类和数值类型的比较
- 包装类的实例可以与数值类型的值进行比较, 这种比较是直接取出包装类实例所包装的数值来进行比较,因为包装类的实例实际上是引用类型, 只有两个包装类引用指向同一个对象时返回true (例子1)
- 注意: 系统把一个 -128~127 之间的整数自动装箱成 Integer 实例, 并放入了一个名为 cache 的数组中缓存了起来; 所以将 -128~127 之间的整数自然数自动装箱成一个 Integer 实例时, 实际上是直接指向对象的数组元素, 因此 -128~127 之间的同一个整数自动装箱成 Integer 实例时, 都是引用 cache 数组的同一个数组元素, 所以他们全部相等(例子2)
- Java7 为所有的包装类提供了一个静态的 compare(xxx val1, xxx val2)方法, 用来比较两个基本类值的大小,(两个boolean 类型的值比较的时候, true > false) (例子3)
示例代码
// 例子1:
System.out.println("比较2个包装类的实例是否相等:" + (new Integer(2) == new Integer(2))); //out:false
Integer a = 6;
System.out.println("6的包装类是否大于5.0: " + (a > 5.0)); // out: true
// 说明:
// 1. "==" 作用于基本类型的变量:则直接比较存储的 "值" 是否相等;
// 作用于引用数据类型的变量: 则比较的是所指向的对象的地址
// 2. "equals" 不能作用于基本数据类型的变量
// 如果没有重写equals方法: 则比较的是引用类型的变量所指向的对象的地址
// 如果String, Date类对 equals 方法重写了的话,比较的是所指向的对象的内容
// 例子2:
// 通过自动装箱, 允许把基本类型的值赋给包装类实例
Integer ina = 2;
Integer inb = 2;
System.out.println("两个2自动装箱后是否相等:" + (ina == inb)); // out:true
Integer biga = 128;
Integer bigb = 128;
System.out.println("两个128自动装箱后是否相等:" + (biga == bigb)); // out:false
// 例子3:
// java7 提供的 compare(xxx val1, xxx val2); 方法
System.out.println(Boolean.compare(true, false)); // out: 1
System.out.println(Boolean.compare(true, true)); // out: 0
System.out.println(Boolean.compare(false, true)); // out: -1
System.out.println(Integer.compare(11, 22)); // out: -1
System.out.println(Integer.compare(11, 2)); // out: 1
Java8 增强的包装类方法(例子4)
- static String toUnsignedString(int/long i)
该方法将指定 int 或 long 型整数转换为无符号整数对应的字符串 - static String toUnsignedString(int/long i, int radix)
该方法将指定 int 或 long 型的整数转换为指定进制的无符号整数对应的字符串 - static xxx parseUnsignedString(String s)
该方法将指定字符串解析成无符号整数, 当调用类为 Integer时, xxx代表int; 当调用类是Long时, xxx代表long - static xxx parseUnsignedString(String s, int radix)
该方法将指定字符串按指定的进制解析成无符号整数, 当调用类为 Integer时, xxx代表int; 当调用类是Long时,
xxx代表long - static int compareUnsigned(xxx x, xxx y)
该方法将 x, y两个整数转换为无符号整数后比较大小, 当调用类为 Integer时, xxx代表int; 当调用类是Long时,
xxx代表long - static long divideUnsigned(long divided, long divisor)
该方法将x, y 两个整数转换为无符号整数后计算他们相除的商, 当调用类为 Integer时, xxx代表int; 当调用类
是Long时,xxx代表long - static long remainderUnsigned(long divided, long divisor)
该方法将x, y 两个整数转换为无符号整数后计算他们相除的余数, 当调用类为 Integer时, xxx代表int; 当调用
类是Long时,xxx代表long
示例代码
// 例子4:
// Java8 增强包装类的无符号算术运算
public class UnsignedTest{
public static void main(String[] args){
byte b = -3;
// 将 byte 类型的 -3 转换为无符号整数
System.out.println("byte类型的-3对应的无符号整数:"
+ Byte.toUnsignedInt(b)); // out: 253
// 指定使用十六进制解析无符号整数
int va1 = Integer.parseUnsignedInt("ab", 16);
System.out.println(va1); // out: 171
// 将 -12转换为无符号 int 类型, 然后转换为十六进制的字符串
System.out.println(Integer.toUnsignedString(-12, 16)); // out: fffffff4
// 将两个数转换为无符号整数后相除
System.out.println(Integer.divideUnsigned(-2, 3)); // out: 1431655764
// 将两个数转换为无符号整数后相除后求余
System.out.println(Integer.remainderUnsigned(-2, 7)); // out: 2
}
}
// 程序说明:
// 1. 无符号整数最大的特点是最高位不再被当成符号位, 因此无符号整数不支持负数, 其最小值为 0
// 2. 理解例子4的关键是先把操作数转换为无符号整数, 然后在进行计算
// 3. 如: byte 类型的 -3, 其原码为:10000011(最高位的1代表负数), 其反码为:11111100, 补码
// 为: 11111101; 将该数当成无符号整数处理, 最高位的1不再代表符号位, 也就是数值位, 该
// 数对应为 253
2. 处理对象
2.1 打印对象 和 toString 方法
- Java对象都是Object 类的实例, 都可直接调用该类中定义的方法, 如: 所有的Java类都具有toString()方法
- 所有的Java对象都可以和字符串进行连接运算, 当Java对象和字符串进行连接运算时, 系统自动调用Java对象 的 toString()方法的返回值和字符串进行连接运算(例子1)
- toString() 方法是一个"自我描述"方法, 通常用于实现这样的功能: 当程序员直接打印该对象时, 系统将会输出该对象的 "自我描述" 信息, 用于告诉外界该对象具有的状态信息
- Object 类提供的 toString()方法总是返回该对象实现类的 "类名 + @ + hashCode" 值, 这个返回值并不能真正实现"自我描述"的功能, 但是可以通过重写Object类的toString()方法来自定义"自我描述"的功能(例子2)
- 重写toString()方法定义返回值格式:(例子3) 类名[field1=值1, Field2=值2,...]
示例代码
// 例子1:
// 打印对象
class Person{
private String name;
public Person(String name){
this.name = name;
}
}
public class PrintObject{
public static void main(String[] args) {
// 创建一个 Person 对象, 赋值给 p 变量
Person p = new Person("shuidaojinshi");
// 打印 P 引用的 Person 对象
System.out.println(p); // out: Person@3b192d32
System.out.println(p.toString()); // out: Person@3b192d32
String pStr = p + "";
System.out.println(pStr); // out: Person@3b192d32
String pStr1 = p.toString() + "";
System.out.println(pStr1); // out: Person@3b192d32
// System.out.println(p.name); // 编译错误的原因是private修饰的只能在当前类使
}
}
// 例子2:
// 自定义toString()方法的返回值(通过重写Object类下的toString()方法)
class Apple{
private String color;
private double weight;
// 无参数构造器
public Apple(){ }
// 提供拥有参数的构造器
public Apple(String color, double weight){
this.color = color;
this.weight = weight;
}
// setColor getColor方法
public void setColor(String color){
this.color = color;
}
public String getColor(){
return this.color;
}
// serWeight getWeight 方法
public void setWeight(double weight){
this.weight = weight;
}
public double getWeight(){
return this.weight;
}
// 重写 toString()方法, 用于实现 Apple 对象的"自我描述"
public String toString(){
return "this is a apple, it's color is : " + getColor() + ", weight is: " + getWeight();
}
}
public class ToStringTest{
public static void main(String[] args){
// 调用的无参构造器
Apple a = new Apple();
// 通过setter方法来赋值
a.setColor("RED");
a.setWeight(5.68);
System.out.println(a); // out: this is a apple, color is: RED, weight is: 5.68
System.out.println(a.toString()); // out: this is a apple, color is: RED, weight is: 5.68
}
}
// 例子3:
// 改写格式化的 Apple类的 toString()方法的返回值
public String toString(){
return "Apple[color = " + color + ", weight = " + weight + "]";
}
2.2 == 和 equals 方法
- == 运算符 和 equals() 方法都是用来测试两个变量是否相等的两种方法
- == 运算符:(例子1)
- 2.1 当两个变量都是基本类型(都是数值类型)时: 则只要两个变量的 值 相等, 才返回 true
- 2.2 当两个都是引用类型的变量时: 只有它们指向同一个对象时, == 判断才返回true,
- 2.3 == 运算符不可用来比较类型上没有父子关系的两个对象
- equals() 方法是Object类提供的一个实例方法, 所有类都可以调用, equals()方法要求两个引用变量指向同一个对象才会返回true, 即Object类的equals()方法比较的结果与==运算符比较的结果完全相等
- String() 重写了Object类的equals()方法:String()的equals()方法判断两个字符串相等的标准是: 只要两个字符串包含的字符序列相同, 通过equals()比较将返回true, 否则将返回 false
正确重写 equals() 方法的条件
- 自反性: 对任意x, x.equals(x) 一定返回 true
- 对称性: 对任意的 x 和 y, 如果 y.equals(x) 返回true, 则 x.equals(y) 也返回true
- 传递性: 对任意的x, y, z, 如果 x.euqals(y)返回ture, y.equals(z)返回true, 则 x.equals(z) 也返回true
- 一致性: 对任意的x, y, 如果对象中用于等价比较傲的信息没有改变, 那么无论调用 x.equals(y)多少次, 返回的结果应该保持一致性, 要么一直是true, 要么一直是false
- 对任意的不是null的x, x.equals(null)一定返回false
示例代码
// 例子1:
// == 来判断两中类型变量是否相等
public class EqualTest{
public static void main(String[] args){
int it = 65;
float f1 = 65.0f;
// 将输出 true
System.out.println("65和65.0f是否相等? " + (it == f1)); // out: 65和65.0f是否相等? true=
char ch = 'A';
// 将输出 true, 因为 == 运算符比较基本类型时比较的时二者的值
System.out.println("65和'A'是否相等? " + (it == ch)); // out: 65和'A'是否相等? true
String str1 = new String("hello");
String str2 = new String("hello");
String str3 = str1;
// 将输出false
System.out.println("str1和str2是否相等? " + (str1 == str2)); // out: str1和str2是否相等? false
// 将输出true, 因为 == 运算符比较引用类型时比较的时二者的引用地址
System.out.println("str1和str3是否相等? " + (str1 == str3)); // out: str1和str3是否相等? true
// 将输出true, 因为 equals()方法比较引用类型比较的时二者的引用地址
System.out.println("str1 是否 equals str2? " + (str1.equals(str2))); // out: str1 是否 equals str2? true
// 由于 java.lang.string 与 EqualTest类没有继承关系
// 所以下面的语句将导致编译错误
// System.out.println("hello" == new EqualTest());
}
}
// 程序说明:
// 1. Java程序直接使用如"Hello"这种字符串直接量(包括可以在编译时就计算出来的字符值)时,JVM将会使用
// 常量池来管理这些字符串
// 2. 当使用 new String("hello"); 时,JVM会先使用常量池来管理"hello"直接量,在调用String类的构造器
// 来创建一个新的 String 对象,新创建的 String 对象被保存在堆内存中,所以,new String("hello");
// 这句代码一共产生了两个字符串对象
// 3. 常量池(constant pool) 专门用于管理在编译时被确定并被保存在已编译的.class文件中的一些数据,它还
// 包括了关于类,方法, 接口中常量,还包括字符串常量。
// 例子2:
// JVM使用常量池管理字符串直接量的情形
public class StringCompareTest{
public static void main(String[] args) {
//s1直接引用常量池中的 "你好世界!"
String s1 = "你好世界";
String s2 = "你好";
String s3 = "世界";
// s4 后面的字符串值可以直接在编译时就确定下来
// s4 直接引用常量池中的 "你好世界!"
String s4 = "你好" + "世界";
// s5 后面的字符串值可以直接在编译时就确定下来
// s5 直接引用常量池中的 "你好世界!"
String s5 ="你" + "好" + "世" + "界";
// s6 后面的字符串不能在编译时候就确定值
// 所以 s6 不能引用常量池的字符串
String s6 = s2 + s3;
// 使用 new 调用构造器将会创建一个新的 String 对象
// s7 引用堆内存中新创建的 String 对象
String s7 = new String("你好世界");
System.out.println(s1 == s4); // out: true
System.out.println(s1 == s5); // out: true
System.out.println(s1 == s6); // out: false
System.out.println(s1 == s7); // out: false
}
}
// 程序说明:
// 1. JVM常量池保证相同的字符串直接量只有一个,不会产生多个副本。例子中 s1, s4, s5所引用的字符串
// 可以在编译期就确定下来,因此他们都将引用常量池中同一个字符串的对象
// 2. 使用 new String()创建的字符串对象是运行时创建出来的,它被保存在堆内存中,不会放入常量池
// 例子3:
// 重写euqals()方法
class Person{
private String name;
private String idStr;
public Person(){}
public Person(String name, String idStr){
this.name = name;
this.idStr = idStr;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
public void setIdStr(String idStr){
this.idStr = idStr;
}
public String getIdStr(){
return this.idStr;
}
// 重写 equals()方法, 提供自定义的相等标准
public boolean equals(Object obj){
// 如果两个对象为同一个对象
if(this == obj)
return true;
// 只有当 obj 是 Person 对象
// obj.getClass() == Person.class 判断obj是否是Person类的实例
if (obj != null && obj.getClass() == Person.class){
Person personObj = (Person)obj;
// 并且当前对象的 idStr 与 对象的 idStr 相等时才可以判断两个对象相等
if (this.getIdStr().equals(personObj.getIdStr())){
return true;
}
}
return false;
}
}
public class OverrideEqualsRight{
public static void main(String[] args) {
Person p1 = new Person("孙悟空", "123456789");
Person p2 = new Person("孙行者", "123456789");
Person p3 = new Person("孙悟饭", "1010001010");
// p1 和 p2 的 idStr 相等, 所以输出true
System.out.println("p1和p2是否相等: " + p1.equals(p2));
// p2 和 p3 的 idStr 不相等, 所以输出 false
System.out.println("p2和p3是否相等: " + p2.equals(p3));
}
}
// 程序说明:
// 1. 用于比较的 equals() 方法是可以重写的,可以加上自己需要的判断规则
// 2. Person类重写了equals()方法,指定了 Person 对象和其他对象相等的标准(equals方法体代码):另一个
// 对象必须是Person类的实例,且两个 Person 对象的 idStr 相等,即可判断两个 Person 对象相等;在
// 这种判断标准下,就是只要两个 Person 对象的身份证字符串相等,即可判断相等
3. 类成员
3.1 理解类成员
- static 关键字修饰的成员就是类成员, 包括: 类变量, 类方法, 静态初始化块, 内部类(包括接口,枚举)
- static 关键字不能修饰构造器, static 修饰的类成员属于整个类, 不属于单个实例
- static 关键字规则: 类成员(包括方法, 初始化块, 内部类和枚举类)不能访问实例成员(包括成员变量, 方法,初始化块, 内部类和枚举类). 因为类成员是属于类的, 类成员的作用域比实例成员的作用域更大, 完全可能出现类成员已经初始化完成, 但实例成员还不曾初始化的情况, 如果允许类成员访问实例成员将会引起大量的错误
类变量
- 类变量属于整个类, 当系统第一次准备使用该类时, 系统会为该类变量分配内存空间, 类变量开始生效,直到该类被卸载, 该类变量占用的内存空间才会被系统的垃圾回收机制回收. 类变量的生存范围几乎是等同于该类的生存范围
- 类变量可以通过 类.类变量 或者 类的对象.类变量 的形式来访问. 对象不拥有对应类的类变量, 当通过对象来访问类变量只是一种假象, 实际上依然访问的是该类的类变量. 所以可以理解为: 当通过对象来访问类变量时,系统会在底层转换为通过该类来访问类变量
- 同一个类的所有对象访问类变量时, 实际上访问的都是该类所持有的变量, 即同一个类的所有实例的类变量共享同一块内存区
类方法
- 类方法也是属于类, 可通过 类.类方法 或 实例.类方法 来调用.
- 与类变量一样, 使用对象来调用类方法, 其效果也与采用类来调用方法完全一样
- 当使用实例来访问类成员时, 实际上依然是委托该类来访问类成员, 即使某个实例为null, 它也可以访问它所属类的类成员(例子1)
- 3 的补充: 如果一个null对象访问实例成员(包括实例方法和实例变量), 将引发 NullPointerExamption 异常,因为null表明这个实例根本不存在, 所以它的实例变量和实例方法自然也就不存在
静态初化块
- 静态初始化块用于执行类初始化动作, 在类的初始化阶段, 系统会调用该类的静态初始化块来对类进行初始化. 一旦该类的初始化结束后, 静态初始化块将永远不会获得执行的机会(静态初始化块就是 static 修饰的初始化块(如:static{代码}),只在第一次加载类的时候只执行一次)
- 静态初始化块 非静态初始化块 构造函数等执行顺序
- 2.1 静态初始化块的优先级最高,也就是最先执行,并且仅在类第一次被加载时执行
- 2.1 非静态初始化块和构造函数后执行,并且在每次生成对象时执行一次;
- 2.3 非静态初始化块的代码会在类构造函数之前执行。因此若要使用,应当养成把初始化块写在构造函数之前的习惯,
便于调试; - 2.4 静态初始化块既可以用于初始化静态成员变量,也可以执行初始化代码;
- 2.5 非静态初始化块可以针对多个重载构造函数进行代码复用。
示例代码
// 例子1:
public class NullAccessStatic{
private static void test(){
System.out.println("static修饰的类方法!");
}
public static void main(String[] args) {
// 定义一个 NullAccessStatic 变量, 其值为 null
NullAccessStatic nas = null;
// 使用 null 对象调用所属类的静态方法
nas.test(); // out: "static修饰的类方法!"
}
}
3.2 单例(Singleton)类
- 如果一个类始终只能创建一个实例, 则这个类被称为单例类(例子1)
- 通过将构造器使用 private 修饰, 从而把该类的所有构造器隐藏起来
- 一旦把该类的构造器隐藏起来, 就需要提供一个 public 方法作为该类的访问点, 用于创建该类的对象, 且该方法必须使用 static 修饰, (因为调用该方法之前还不存在对象, 因此调用该方法的不可能是对象, 只能是类)
- 该类还必须缓存已经创建的对象, 否则该类无法知道是否曾经创建过对象, 也就无法保证只创建一个对象. 为此该类需要使用一个成员变量来保存曾经创建的对象, 因为该成员变量需要被上面的静态方法访问, 所以成员变量必须使用 static 修饰
示例代码
// 例子1:
class Singleton{
// 使用一个类变量来缓存曾经创建的实例
private static Singleton instance;
// 对构造器使用 private 修饰, 隐藏该构造器
private Singleton(){}
// 提供一个静态方法, 用于返回 Singleton 实例
// 该方法可以加入自定义控制, 保证只产生一个 Singleton 对象
public static Singleton getInstance(){
// 如果 instance 为 null, 则表明还不曾创建 Singleton 对象
// 如果 instance 不为 null, 则表明已经创建了 Singleton 对象
// 将不会创建新的实例
if (instance == null){
// 创建一个 Singleton 对象, 并将其缓存
instance = new Singleton();
}
return instance;
}
}
public class SingletonTest{
public static void main(String[] args) {
// 创建 Singleton 对象不能通过构造器
// 只能通过 getInstance 方法来得到实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // out: true
}
}
// 程序说明:
// 1. 上面的例子中通过 private 修饰构造器, 从而隐藏了该类创建对象的渠道
// 2. 通过 public 修饰的 getInstance 方法提供的自定义控制(这也是封装的优势:不允许自由访问类的成员变量和
// 实现细节,而是通过方法来控制合适的暴露),从而保证 Singleton 类只能产生一个实例。所以在 Singleton
// 类的 main() 方法中,看到两次产生的 Singleton 对象实际上是同一个对象
4. final 修饰符
4.1 final 成员变量
- final 关键字可用于修饰类,变量和方法,用于表示修饰的类,方法和变量不可改变
- final 修饰变量时,表示该变量一旦获得了初始值就不可改变(不能被重新赋值),final 可修饰成员变量(类变量和实例变量)也可修饰局部变量和形参
- final 修饰的成员变量必须由程序员显式的指定初始值(例子1)
- 类变量:必须在静态初始化块中指定初始值或者在声明该类变量时指定初始值,而且只能在这两个地方的其中之一指定
- 实例变量:必须在非静态初始化块,声明该实例变量或构造器中指定初始值, 而且只能在这三个地方的其中之一指定
- 实例变量不能在静态初始化块中指定初始值,因为静态初始化块时静态成员,不可访问实例变量--非静态成员
- 类变量也不能在普通初始化块中指定初始值,因为类变量在类初始化阶段已经被初始化了, 普通初始化块不能再对其重新赋值
- 如果在构造器或初始化块中对 final 成员变量进行初始化,那么在初始化之前不能访问成员变量的值, 否则报错(例子2)
示例代码
// 例子1:
// final 修饰成员变量的效果
public class FinalVariableTest{
// 定义成员变量时指定默认值,合法
final int a = 6;
// 下面的变量将在构造器或者初始化块中分配初始值
final String str;
final int c;
// 下面的类变量没有指定初始值,只能在静态初始化块中指定初始值
final static double d;
// 既没有指定默认值,又没有在初始化块或构造器中指定初始值
// 下面定义的 ch 实例变量不合法
// final char ch;
// 初始化块, 可对没有指定默认值的实例变量指定初始值
{
// 在初始化块中为实例变量指定初始值, 合法
str = "hello";
// 定义a重新赋值, 因此下面的语句非法
// a = 9;
System.out.println("普通初始化块");
}
// 静态初始化块中为类变量指定初始值, 合法
static {
d = 5.6;
System.out.println("静态初始化块");
}
// 构造器中既可以对没有指定默认值, 又没有在初始化块中指定默认值的实例变量指定初始值
public FinalVariableTest(){
// 如果在初始化块中已经为 str 指定了初始值
// 那么在构造器中不能对 final 变量重新赋值,下面的赋值语句非法
// str = "java";
c = 5;
System.out.println("构造器");
}
public static void main(String[] args) {
FinalVariableTest ft = new FinalVariableTest();
System.out.println(ft.a);
System.out.println(ft.c);
//The static field FinalVariableTest.d should be accessed in a static way
System.out.println(FinalVariableTest.d);
}
}
// 例子2:
// 在普通初始化块中进行初始化之前访问 final 修饰的实例变量的值,报错
public class FinalErrorTest{
// 定义一个 final 修饰的实例变量, 定义时未指定值
// 系统不会为 final 修饰的成员变量进行默认的初始化
final int age;
// 普通初始化块
{
// age 没有初始化, 所以此处访问age的值报错
// The blank final field age may not have been initialized
// System.out.println(age);
age = 6;
System.out.println(age);
}
public static void main(String[] args){
new FinalErrorTest();
}
}
4.2 final 局部变量
- 系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化
- final 修饰的局部变量,既可在定义时指定默认值(定义时指定,后面的代码中不能再赋值),也可不指定默认值,在后面的代码中对该 final 变量赋初始值,但只能一次, 不可重复。
- final 修饰形参时,因为形参在调用该方法时,由系统根据传入的参数来完成初始化,因此使用 final 修饰的形参不能被赋值
示例代码
// 例子:
public class FinalLocalVariableTest{
public void test(final int a){
// 不能对 final 修饰的形参赋值,下面的语句非法
// a = 5;
System.out.println("不能对 final 修饰的形参赋值, 需由调用时传入参数完成初始化!" + a);
}
public static void main(String[] args) {
// 定义 final 局部变量时指定默认值,则 str 变量无法重新赋值
final String str = "Hello";
// 下面的语句非法
// str = "java";
// 定义局部变量时没有指定默认值,则 d 变量可被赋值一次(仅一次)
final double d;
// 第一次赋值
d = 5.6;
// 对 final 修饰的局部变量第二次赋值,下面语句非法
// d = 3.4;
System.out.println(str + " " + d);
FinalLocalVariableTest fl = new FinalLocalVariableTest();
fl.test(100);
fl.test(250);
}
}
4.3 final 修饰基本类型变量和引用类型变量的区别
- 使用 final 修饰基本类型变量时, 不能对基本类型变量重新赋值,因此基本类型变量不能被改变
- 使用 final 修饰引用类型,因为引用类型保存的仅仅是一个引用,final 只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可能发生改变
示例代码
// 例子:
import java.util.Arrays;
class PersonFinal {
private int age;
public PersonFinal(){}
// 有参数的构造器
public PersonFinal(int age){
this.age = age;
}
// setAge 和 getAge 方法
public void setAge(int age){
this.age = age;
}
public int getAge(){
return this.age;
}
}
public class FinalReferenceTest{
public static void main(String[] args) {
// final 修饰数组变量,iArr 是一个引用变量
final int[] iArr = {5, 6, 9, 12, 2, 55, -1};
System.out.println(Arrays.toString(iArr)); // out: [5, 6, 9, 12, 2, 55, -1]
// 对数组元素进行排序, 合法
Arrays.sort(iArr);
System.out.println(Arrays.toString(iArr)); // out: [-1, 2, 5, 6, 9, 12, 55]
// 对数组元素赋值,合法
iArr[2] = -8;
System.out.println(Arrays.toString(iArr)); // out: [-1, 2, -8, 6, 9, 12, 55]
// 下面语句对 iArr 重新赋值,非法
// iArr = null;
// final 修饰 PersonFinal 变量, p 是一个引用变量
final PersonFinal p = new PersonFinal(45);
// 改变 PersonFinal 对象的 age 实例变量,合法
p.setAge(23);
System.out.println(p.getAge()); // out: 23
// 下面的语句对 p 重新赋值,非法
// p = null;
}
}
// 程序说明:
// 1. 使用 final 修饰的引用类型变量不能被重新赋值,但可以改变引用类型变量所引用对象的内容。 例如上面 iArr 变量所引用的数组
// 对象, final 修饰后的 iArr 变量不能被重新赋值,但是 iArr 所引用数组的数组元素可以改变。
// 2. p 变量也使用 final 修饰,表明 p 变量不能被重新赋值,但 p 变量所引用的 PersonFinal 对象的成员变量的值可以被改变
4.4 可执行"宏替换"的final变量
- 对于一个final变量来说,不管它是类变量,实例变量,还是局部变量,只要该变量满足下面三个条件,那么这个final变量就不再是一个变量,而是相当于一个直接量(例子1)
- 1.1 使用 final 修饰符修饰
- 1.2 在定义该 final 变量时指定了初始值
- 1.3 该初始值可以在编译时就被确定下来
- final修饰符的一个重要用途就是定义"宏变量"。当定义 final 变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个 final 变量本质上就是一个 "宏变量",编译器会把程序中所有用到该变量的地方直接替换成该变量的值
- 对于 final 实例变量而言,只有定义该变量时指定了初始值才会有 "宏变量" 的效果
- Java会使用常量池来管理曾经用到过的字符串直接量,例如执行:String a = "Java"; 语句之后,常量池中就会缓存一个字符串"Java";如果程序再次执行String b = "Java"; 系统就会让 b 直接指向常量池中的 "Java" 字符串, 因此 a == b 将会返回 True (例子2)
示例代码
// 例子1:
// 满足说明1的final修饰的变量相当于一个直接量
public class FinalLocalTest{
public static void mian(String[] args){
// 定义一个普通的局部变量
final int a = 5;
System.out.println(a);
}
}
// 程序说明:
// 1. 对于上面的程序,变量 a 其实根本不存在,当程序执行System.out.println(a);代码时,实际转换为执行 S
// ystem.out.println(a5;
// 例子2:
public class FinalABTest{
public static void main(String[] args){
final String a = "Java";
final String b = "Java";
System.out.println("a == b :" + (a == b)); // out: a == b :true
System.out.println("a.equals(b): " + a.equals(b)); // out: a.equals(b): true
}
}
// 例子3:
public class StringJoinTest{
public static void main(String[] args) {
String s1 = "学 Java";
// s2变量引用的字符串可以在编译时就确定下来
// 因此 s2 直接引用常量池已有的 "学 Java" 字符串
String s2 = "学 " + "Java";
System.out.println("s1 == s2: " + (s1 == s2)); // out: s1 == s2: true
// 定义两个字符串直接量
String str1 = "学 ";
String str2 = "Java";
String s3 = str1 + str2;
System.out.println("s1 == s3: " + (s1 == s3)); // out: s1 == s3: false
}
}
// 程序说明:
// 1. 对于 s1 和 s2 编译器在编译时就可以确定他们的值,所以系统会让 s2 直接指向常量池中缓存的字符串
// 2. 而 s3 的值是由 str1 和 str2 进行连接运算后得到的,由于 str1 和 str2 是普通变量,编译器不会执行 "宏替
// 换",所以在编译时 s3 的值时无法确定的,也就无法让 s3 指向常量池中缓存的字符串
// 3. 让 s1 == s3 为 true 很简单,只要使用 final 修饰 str1 和 str2 两个变量就可以执行 "宏替换",从而在编译
// 时就确定s3的值,进而让 s3 也指向常量池中缓存的 字符串
4.5 final 方法
- final 修饰的方法不可被重写,如:Object类里的 getClass()方法,因为Java不希望任何类重写这个方法,所以使用 final 修饰,从而将这个方法密封起来(例子1)
- 对于一个 private 修饰的方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法,
- rewiew: 如果子类中定义了一个与父类 private 方法由相同方法名,相同形参列表,相同返回值类型的方法,也不是重写父类的 private 方法,而是子类重新定义了一个新方法。因此:即使使用 final 修饰一个 private 访问权限的方法,依然可以在其子类中定义与该方法具有相同方法名,相同形参列表,相同返回值类型的方法(例子2)
- final 修饰的方法仅仅是不能被重写,并不是不能被重载(例子3)
示例代码
// 例子1:
public class FinalMethodTest{
public final void test(){
System.out.println("final 修饰的 Public 访问权限的 test() 方法");
}
}
class sub extends FinalMethodTest{
// 下面方法定义将出现编译错误,不能重写父类 final 方法
public void test(){}
}
// 例子2:
// 子类定义了和父类(被 private 和final 修饰的) 相同方法名,相同形参列表,相同返回值类型的方法,但不是重写父类方法,是子类重新定义
public class PrivateFinalMethodTest{
private final void test(){}
}
class sub extends PrivateFinalMethodTest{
// 下面的方法定义不会出现问题
public void test(){}
}
// 例子3:
public class FinalOverload{
// final 修饰的方法只是不能被重写,完全可以被重载
public final void test(){}
public final void test(String args){}
}
4.6 final 类
- final 修饰的类不可以有子类,如java.lang.Math 类就是一个 final 类,它不可以有子类
- 也就是说 final 修饰的类不可以被继承
示例代码
// 例子:
public final class FinalClass{}
// 下面定义的类继承FinalClass类将出现编译错误
// class Sub entends FinalClass{}
4.7 不可变类
- 不可变(immutable)类的意思是创建该类的实例后, 该实例的实例变量不可变
- Java 的8个包装类和java.lang.String类都是不可变类,当创建他们的实例后, 其实例的实例变量不可改变
- 与不可变类对应的是可变类, 可变类的含义是该类的实例变量是可变的. 大部分时候创建的类都是可变类
- 不可变类的实例在整个生命周期中永远处于初始化状态, 它的实例变量不可改变
- 问题: 当创建不可变类时, 如果它包含成员变量的类型是可变的, 那么其对象的成员变量的值依然是可变的--这个不可变类其实是失败的
- 设计不可变类时, 尤其要注意引用类型的成员变量, 如果引用类型的成员变量的类是可变的,就必须采取必要的措施来保护该成员变量所引用的对象不是被修改,这样才能创建真正的不可变类 (例子2 多看)
创建自定义不可变类的规则
- 类添加 final 修饰符, 保证类不被继承
- 使用 private 和 final 修饰符来修饰该类的成员变量
- 提供带参数构造器, 用于根据传入参数来初始化类里的成员变量
- 仅为该列的成员变量提供 getter 方法, 不为该类的成员变量提供 setter 方法, 因为普通方法无法修改 final 修饰的成员变量
- 在 getter 方法中, 不要直接返回对象本身, 而是克隆对象, 并返回对象的拷贝
- 如果有必要,重写 Object 类的 hashCode() 方法和 equals() 方法,equals()方法根据关键成员变量来作为两个对象相等的标准,除此之外,还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等(例子4)
示例代码
// 例子1:
// 定义一个不可变的 Address 类, 程序把 Address 类的 detail 和 postCode 成员变量都是用 private 隐藏起来, 并
// 使用 final 修饰这两个成员变量, 不允许其他方法修改这两个成员变量的值
public class Address{
// 使用 private 和 final 修饰成员变量
private final String detail;
private final String postCode;
// 构造器里初始化两个实例变量
public Address(){
this.detail = "";
this.postCode = "";
}
public Address(String detail, String postCode){
this.detail = detail;
this.postCode = postCode;
}
// 仅为这两个实例变量提供 getter 方法
public String getDetail(){
return this.detail;
}
public String getPostCode(){
return this.postCode;
}
// 重写 equals() 方法, 判断两个对象是否相等
public boolean equals(Object obj){
if (this == obj){
return true;
}
if (obj != null && obj.getClass() == Address.class){
Address ad = (Address)obj;
// 当 detail 和 postCode 相等时, 可认为两个 Address 对象相等
if (this.getDetail().equals(ad.getDetail()) && this.getPostCode().equals(ad.getPostCode())){
return true;
}
}
return false;
}
public int hashCOde(){
return detail.hashCode() + postCode.hashCode() * 31;
}
}
// 程序说明:
// 1. 对于上面的 Address 类, 程序创建了 Address 对象后, 同样无法修改 Address 对象的 detail 和 postCode
// 实例变量
// 例子2:
class Name{
private String firstName;
private String lastName;
public Name(){}
public Name(String firstName, String lastName){
this.firstName = firstName;
this.lastName = lastName;
}
// firstName 和 laseName 的 setter 和 getter 方法
public void setFrisrtName(String firstName){
this.firstName = firstName;
}
public String getFirstName(){
return this.firstName;
}
public void setLastName(String lastName){
this.lastName = lastName;
}
public String getLastName(){
return this.lastName;
}
}
public class immutablePerson{
private final Name name;
public immutablePerson(Name name){
this.name = name;
}
public Name getName(){
return name;
}
public static void main(String[] args){
Name n = new Name("悟空", "孙");
immutablePerson p = new immutablePerson(n);
// immutablePerson 对象的 name 的 firstName 值为 "悟空"
System.out.println(p.getName().getFirstName()); // out: 悟空
// 改变 immutablePerson 对象的 name 的 finstName 值
n.setFrisrtName("八戒");
// immutablePerson 对象的 name 的 firstName 值被改为"八戒"
System.out.println(p.getName().getFirstName()); // out: 八戒
}
}
// 程序说明:
// 1. n.setFrisrtName("八戒");这句代码修改了Name对象(可变类的实例)的firstName的值,单由于 immutablePerson
// 类的 name 实例变量引用了该 Name 对象, 这就会导致 immutablePerson 对象的 name 的 firstName 会改变,
// 这就破坏了 immutablePerson 类的初衷
// 2. 为了保持immutablePerson 对象的不可变性, 必须保护好 immutablePerson 对象的引用类型的成员变量: name,
// 让程序无法访问到 immutablePerson 对象的 name 成员变量, 也就无法利用 name 成员变量的可变性来改变
// immutablePerson 对象了. 如例子3修改immuablePerson类
// 例子3:
// 修改immuablePerson类
class Name{
// ......
}
public class immutablePerson{
private final Name name;
public immutablePerson(Name name){
// 设置 name 实例变量为临时创建的 Name 对象, 该对象的 firstName 和 lastName 与
// 传入的 name 参数的 firstName 和 lastName 相同
this.name = new Name(name.getFirstName(), name.getLastName());
}
public Name getName(){
// 返回一个匿名对象, 该对象的 firstName 和 lastName 与
// 该对象里的 name 的 firstName 和 lastName 相同
return new Name(name.getFirstName(), name.getLastName());
}
public static void main(String[] args){
Name n = new Name("悟空", "孙");
immutablePerson p = new immutablePerson(n);
// immutablePerson 对象的 name 的 firstName 值为 "悟空"
System.out.println(p.getName().getFirstName()); // out: 悟空
// 改变 immutablePerson 对象的 name 的 finstName 值
n.setFrisrtName("八戒");
// immutablePerson 对象的 name 的 firstName 值被改为"八戒"
System.out.println(p.getName().getFirstName()); // out: 悟空
}
}
// 程序说明:
// 1. 当程序向 immutablePerson 构造器传入一个Name对象时,该构造器创建 immutablePerson 对象时并不是
// 直接利用已有的 Name 对象(利用已有的 Name 对象有风险,因为这个已有的Name对象是可变的,如果程序改变
// 了这个Name对象,将会导致immuablePerson 对象也发生变化), 而是重新创建了一个 Name 对象来赋给
// immutablePerson 对象的 name 实例变量. 当 immutablePerson 对象返回 name 变量时, 它并没有直接
// 把name实例变量返回, 直接返回name实例变量的值也可能导致它所引用的Name对象被修改
// 例子4:
public class ImmutableStringTest{
public static void main(String[] args) {
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2); // out: false
System.out.println(str1.equals(str2)); // out: true
// 下面两次输出的hashCode 相同
System.out.println(str1.hashCode()); // out: 99162322
System.out.println(str2.hashCode()); // out: 99162322
}
}
4.8 缓存实例的不可变类
- 不可变类的实例状态不可改变,可以很方便的被多个对象所共享; 如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例
- 缓存是软件设计中的一个非常有用的模式, 缓存的方式有很多种,下面的例子使用数组来作为缓存池,从而实现一个缓存实例的不可变类
示例代码
// 例子:
class CacheImmutale{
private static int MAX_SIZE = 10;
// 使用数组来缓存已有的实例
private static CacheImmutale[] cache
= new CacheImmutale[MAX_SIZE];
// 记录缓存实例在缓存中的位置, cache[pos-1]是最新缓存的实例
private static int pos = 0;
private final String name;
private CacheImmutale(String name){
this.name = name;
}
public String getName(){
return this.name;
}
public static CacheImmutale valueOf(String name){
// 遍历已缓存的对象
for (int i = 0; i < MAX_SIZE; i++){
// 如果已有相同的实例, 则直接返回该缓存的实例
if (cache[i] != null && cache[i].getName().equals(name)){
return cache[i];
}
}
// 如果缓存池已满
if (pos == MAX_SIZE){
// 把缓存的第一个对象覆盖, 即把刚刚生成的对象放在缓存吃的最开始位置
cache[0] = new CacheImmutale(name);
// 把 pos 设为 1
pos = 1;
}
else{
// 把新创建的对象缓存起来, pos 加 1
cache[pos++] = new CacheImmutale(name);
}
return cache[pos - 1];
}
public boolean equals(Object obj){
if (this == obj){
return true;
}
if (obj != null && obj.getClass() == CacheImmutale.class){
CacheImmutale ci = (CacheImmutale)obj;
return name.equals(ci.getName());
}
return false;
}
public int hashCode(){
return name.hashCode();
}
}
public class CacheImmutaleTest{
public static void main(String[] args) {
CacheImmutale c1 = CacheImmutale.valueOf("hello");
CacheImmutale c2 = CacheImmutale.valueOf("hello");
// 下面的代码将输出true
System.out.println(c1 == c2); // out: true
}
}
// 程序说明:
// 1. 上面的 CacheImmutale 类使用一个数组来缓存该类的对象,这个数组的长度是 MAX_SIZE, 即该类共可以缓存
// MAX_SIZE个CacheImmutale 对象. 当缓存池已满时, 缓存池采用 "先进先出" 规则来决定那个对象被移出
// 缓存池
// 2. 当使用 CacheImmutale 类的 valueOf() 方法来生成对象时, 系统会进行判断, 如果该数组中已经缓存了该类
// 的对象, 系统将不会重新生产对象
// 3. CacheImmutale 类能控制系统 CacheImmutale 对象的个数, 需要程序使用该类的 valueof() 方法来得到其
// 对象, 而且程序使用 private 修饰符隐藏该类的构造器, 因此程序只能通过该类提供的 valueof() 方法来
// 获得实例
5. 抽象类
5.1 抽象方法和抽象类
- 抽象方法只有方法签名,没有方法实现的方法
- 抽象方法和抽象类必须使用 abstract 修饰符来定义,有抽象方法的类只能被定义为抽象类,抽象类里可以没有抽象方法
- 归纳:抽象类可用“有得有失”来描述:"得"指的是抽象类多了一个能力:抽象类可包含抽象方法;"失"指的是抽象类失去了一个能力:抽象类不能用于创建实例
- 定义抽象类只需在普通类上增加 abstract 修饰符即可,甚至是一个普通类(没有包含抽象方法的类)增加了 abstract 修饰符后也将变成抽象类
- 定义抽象方法只需要在普通方法上增加 abstract 修饰符,并把普通方法的方法体部分去掉,并在方法体后面增加分号即可
- 5.1 如:抽象方法:public abstract void test(); 抽象方法,没有方法体(即方法定义后面没有一对花括号)
- 5.2 如:普通方法:public void test(){} 普通方法,定义了方法体,但方法体为空
抽象方法和抽象类的规则
- 抽象类和抽象方法必须使用abstract修饰符来修饰,抽象方法不能有方法体
- 抽象类不能被实例化,无法使用 new 关键字来调用抽象类的构造器创建抽象类的实例,即使抽象类里不包含抽象方法,这个抽象类也不能创建实例
- 抽象类可以包含成员变量,方法(普通方法和抽象方法都可以),构造器,初始化块,内部类(接口,枚举类)5部分,抽象类的构造器不能用于创建实例,主要是用于被其子类调用
- 含有抽象方法的类(包括直接定义了一个抽象方法,或者继承了一个抽象父类,但没有完全实现父类包含的抽象方法,或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义为抽象类
注意
- abstract 修饰类时,表明这个类只能被继承; abstract 修饰方法时: 表明这个方法必须由子类提供实现(重写)
- 由于 final 修饰的类不能被继承,final 修饰的方法不能被重写。 所以 final 和 abstract 互斥,不能共存
- abstract 也不能用于修饰成员变量,不能用于修饰局部变量,也不能用于修饰构造器
- static 修饰的方法是类方法,即通过类可调用该方法,但如果该方法被定义成了抽象方法,则将导致通过该类来调用方法是出现错误(因为调用了一个没有方法体的方法),所以 static 和 abstract 也互斥(内部类可以共存),不能同时修饰某个方法
- abstract 关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此 abstract 方法也不能定义为 private 的访问权限,即 abstract 和 private 关键字也是互斥 abstract 和 final private static 这三个关键字互斥(暂理解),不能同时使用
示例代码
// 例子1:
// 定义一个 Shape 抽象类
public abstract class Shape{
{
System.out.println("abstract修饰的抽象类,执行 Shape 的普通初始化块。。。");
}
private String color;
// 定义一个计算周长的抽象方法
public abstract double calPerimeter();
// 定义一个返回形状的抽象方法
public abstract String getType();
// 定义 Shape 的构造器,该构造器并不是用于创建 Shape 对象,而是用于被子类调用的
public Shape(){}
public Shape(String color){
System.out.println("执行 Shape 的构造器。。。");
this.color = color;
}
// 定义 color 的 getter 和 setter 方法
public void setColor(String color){
this.color = color;
}
public String getColor(){
return this.color;
}
}
// 程序说明:
// 1. 抽象的 Shape 类中定义了 两个 抽象方法 calPerimeter() 和 getType()
// 2. Shape 类里既包括了初始化块,也包含了构造器,这些都不是在创建 Shape 对象时被调用的,而是在
// 创建其子类的实例的时被调用
// 例子2:
// 定义一个三角形普通类,继承 Shape 类, 必须实现 Shape 类的所有抽象方法
public class Triangle extends Shape{
// 定义三角形的三条边
private double a;
private double b;
private double c;
public Triangle(String color, double a, double b, double c){
super(color);
this.setSides(a, b, c);
}
public void setSides(double a, double b, double c){
if (a >= b + c || b >= a + c || c >= a + b){
System.out.println("三角形两边之和必须大于第三边");
return;
}
this.a = a;
this.b = b;
this.c = c;
}
// 重写 Shape 类的计算周长的抽象方法
public double calPerimeter(){
return a + b + c;
}
// 重写 Shape 类的返回形状的方法
public String getType(){
return "三角形";
}
}
// 程序说明:
// 1. 继承了Shape类,并实现了Shape类的两个抽象方法,是一个普通类,因此可以创建 Triangle 类的实例
// 可以让一个 Shape 类型的引用变量指向 Triangle 对象
// 例子3:
// 再定义一个 Cricle 普通类,继承 Shape 类
public class Circle extends Shape{
private double radius;
public Circle(String color, double radius){
super(color);
this.radius = radius;
}
public void setRadius(double radius){
this.radius = radius;
}
// 重写 Shape 类的计算周长的抽象方法
public double calPerimeter(){
return 2 * Math.PI * radius;
}
// 重写 Shape 类的返回形状的抽象方法
public String getType(){
return getColor() + " 圆形";
}
public static void main(String[] args) {
Shape s1 = new Triangle("黑色", 3, 4, 5);
Shape s2 = new Circle("黄色", 3);
System.out.println(s1.getType());
System.out.println(s1.calPerimeter());
System.out.println(s2.getType());
System.out.println(s2.calPerimeter());
}
}
// 程序说明:
// 1. main() 方法中定义了两个 Shape 类型的引用变量,他们分别指向 Triangle 对象和 Circle 对象
// 2. 由于在 Shape 类中定义了 calPerimeter() 方法和 getType() 方法,所以程序可以直接调用 s1 变量
// 和 s2 变量的 calPerimeter() 方法和 getType() 方法, 无需强制类型转换为其子类类型
5.2 抽象类的作用
- 抽象类不能创建实例, 只能当成父类来被继承; 抽象类是从多个具体类中抽象出来的父类, 它具有更高层次的抽象
- 抽象类是一种常见的简单的模版模式设计模式之一
模版模式设计规则
- 抽象父类可以只定义需要使用的某些方法, 把不能实现的部分方法抽象成抽象方法, 留给其子类去实现
- 父类中可能包含需要使用其他系列方法的方法, 这些被调方法既可以由父类实现, 也可以由其子类实现
示例代码
// 例子1:
// 模版模式的范例
public abstract class SpeedMeter{
// 定义转速
private double turnRate;
public SpeedMeter(){
}
// 把返回车轮半径的方法定义为抽象方法
public abstract double getRadius();
public void setTurnRate(double turnRate){
this.turnRate = turnRate;
}
// 定义计算速度的通用公式
public double getSpeed(){
// 速度等于车轮半径 * 2 * PI * 转速
return java.lang.Math.PI * 2 * getRadius() * turnRate;
}
}
// 程序说明:
// 1. 上面程序定义了一个 抽象类 abstract SpeedMeter, 里面定义了两个通用方法, setTurnRate 和
// getSpeed 方法, 还定义了一个抽象方法, getRadius()
// 2. 抽象类 abstract SpeedMeter,只能被继承, 并且抽象方法 getRadius()必须在子类中实现其方法
// 例子2:
public class CarSpeedMeter extends SpeedMeter{
public double getRadius(){
return 0.28;
}
public static void main(String[] args) {
CarSpeedMeter csm = new CarSpeedMeter();
csm.setTurnRate(15);
System.out.println(csm.getSpeed()); // out: 26.389378290154266
}
}
// 程序说明:
// 1. CarSpeedMeter 类继承了 SpeedMeter这个抽象方法, 并且实现了抽象方法 getRadius,
// 2. CarSpeedMeter 是一个普通类, 可以创建类的对象, 也可以调用父类的方法实现获取当前速度
6. Java8改进的接口
6.1 接口的概念
- 接口(interface) 是一种更加特殊的"抽象类"; 接口里不能包含普通方法, 接口里的所有方法都是抽象方法;Java8 允许在接口中定义默认方法, 默认方法可以提供方法实现
- 接口定义了一种规范, 接口定义了某一批类所需要遵守的规范, 接口不关心这些类的内部状态数据, 也不关心这些类里方法的实现细节, 接口只规定这批类里必须提供某些方法, 提供这些方法的类就可以满足实际需要; 而类是一种具体实现体
- 接口是从多个相似类中抽象出来的规范, 接口不提供任何实现, 接口体现的是规范和实现分离的设计哲学, 是一种松耦合的设计
- 接口定义的是多个类共同的公共行为规范, 这些行为是与外部交流的通道, 这就意味着接口里通常是定义一组公用的方法
6.2 Java8中接口的定义
- 修饰符可以是 public 或者省略, 如果省略了 public 访问控制符, 则默认采用包访问权限访问控制符, 即只有在相同的包结构下才可以访问该接口
- 接口名应与类名采用相同的命名规则, 多个有意义的单词连缀而成,单词首字母大写
- 只有在Java8以上的版本中才允许在接口中定义默认方法, 类方法; 接口中的默认方法, 默认是使用 default 修饰, 该方法不能使用 static 修饰
- 由于接口定义的是一种规范, 因此接口里不能包含构造器和初始化块
- 接口里可包含:成员变量(只能是静态常量), 方法(只能是抽象实例方法, 类方法或默认方法), 内部类(包括内部接口和枚举)
- 接口里的访问权限都是 public 访问权限
- 接口中的成员变量, 不管是否使用 public final static 修饰符,接口里的成员变量总是使用这三个修饰符来修饰
- 因为接口里没有构造器和初始化块, 所以接口里定义的成员变量只能在定义时指定默认值(例子1)
- 接口里的普通方法总是使用 public abstract 来修饰, 接口里的普通方法不能有方法体; 但是类方法, 默认方法都必须有方法体
- 接口里定义的内部类, 内部接口, 内部枚举默认都是采用 public static 修饰符, 不管定义时是否指定这两个修饰符, 系统都是自动使用 public static 对他们进行修饰
- 接口可被当成一个特殊的类, 因此一个 Java 源文件里最多只能有一个 public 接口, 且这个源文件的主文件名必须和 public 接口名相同
接口定义的基本语法(例子2)
[修饰符] interface 接口名 extends 父接口1, 父接口2...{
零个到多个常量定义...
零个到多个抽象方法定义...
零个到多个内部类, 接口, 枚举定义...
零个到多个默认方法或类方法定义...
}
示例代码
// 例子1:
// 系统自动为接口里定义的成员变量增加 public static final 修饰符
int MAX_SIZE = 50;
public static final int MAX_SIZ = 50;
// 例子2:
// 定义一个接口
public interface Output{
// 接口里定义的成员变量只能是常量
int MAX_CACHE_LINE = 50;
// 接口里定义的普通方法只能是抽象方法
void out();
void getData(Strng msg);
// 在接口中定义默认方法,需要使用 default 修饰
default void print(String... msgs){
for (String msg : msgs){
System.out.println(msg);
}
}
// 在接口中定义默认方法, 需要使用 default 修饰
default void test(){
System.out.println("默认的 test() 方法");
}
// 在接口中定义类方法, 需要使用 static 修饰
static String staticTest(){
return "接口里的类方法";
}
}
// 程序说明:
// 1. 上面程序定义了一个 Output 接口, 包含一个成员变量: MAX_CACHE_LINE, 还定义了两个普通方法, 取
// 数据的 getData() 方法和表示输出的 out() 方法,
// 2. 如上程序: 定义了 Output 接口的规范: 只要某个类能取的数据, 并可以将数据输出, 那它就是一个输出
// 设备
6.3 接口的继承
- 接口和类的区别就是接口完全支持多继承; 即一个接口可以有多个直接父接口(例子1)
- 和类继承相似, 子接口扩展某个父接口, 将会获得父接口的所有抽象方法和常量
示例代码
// 例子1:
interface interfaceA{
int PROP_A = 5;
void testA();
}
interface interfaceB{
int PROP_B = 6;
void testB();
}
interface interfaceC extends interfaceA, interfaceB{
int PROP_C = 7;
void testC();
}
public class InterfaceExtendsTest{
public static void main(String[] args) {
System.out.println(interfaceC.PROP_A);
System.out.println(interfaceC.PROP_B);
System.out.println(interfaceC.PROP_C);
}
}
// 程序说明:
// 1. 程序中 interfaceC 接口继承了 interfaceA 和 interfaceB, 所以 interfaceC 中获得了他们的常量
// 所以在 main() 方法中可以通过 interfaceC 来访问 PROP_A, PROP_B 和 PROP_C 常量
6.4 使用接口
- 接口不能用于创建实例, 但接口可以用于声明引用类型变量
- 使用接口来声明引用类型变量时, 这个引用类型的变量必须引用到其实现类的对象
- 一个类可以实现一个或多个接口, 继承使用 extends 关键字, 实现则用 implements 关键字
- 实现接口方法时, 必须使用 public 访问控制符, 因为接口里的方法都是 public 的, 而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等, 所以实现类实现接口里的方法只能使用 public 访问权限
- 接口不能显式的继承任何类, 但所有接口类型的引用变量都可以直接赋值给 Object 类型的引用变量
接口的用途
- 定义变量, 也可用于进行强制类型转换
- 调用接口中定义的常量
- 被其他类实现
类实现接口的语法
[修饰符] class 类名 extends 父类1 implements 接口1, 接口2...{
// 类体部分
}
语法说明:
1. 实现接口和继承父类相似,一样可以获得所实现接口里定义的常量(成员变量), 方法(包括抽象方法和
默认方法)
2. 让类实现接口需要类定义后增加 implements 部分, 当需要实现多个接口时, 多个接口之间使用英文逗号
隔开,
3. 一个类只能继承一个父类, 但可以继承多个接口, implements 部分必须放在 extends 部分之后
4. 一个类实现了一个或多个接口之后, 这个必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些
抽象方法); 否则, 该类将保留从父类接口里继承到的抽象方法, 该类也必须定义成一个抽象类
示例代码
// 例子:
// 定义一个 Product 接口
interface Product{
int getProductTime();
}
// 让 Printer 类实现 Output 和 Product 接口
public class Printer implements Output, Product{
private String[] printData = new String[MAX_CACHE_LINE];
// 用以记录当前需打印的作业数
private int dataNum = 0;
public void out(){
// 只要有作业, 就继续打印
while (dataNum > 0){
System.out.println("打印机打印: " + printData[0]);
// 把作业队列整体前移一位, 并将剩下额作业数减 1
System.arraycopy(printData, 1, printData, 0, --dataNum);
}
}
public void getData(String msg){
if (dataNum >= MAX_CACHE_LINE){
System.out.println("输出队列已满, 添加失败");
}
else{
// 把打印数据添加到队列, 已保存数据的数量加1
printData[dataNum++] = msg;
}
}
public int getProductTime(){
return 45;
}
public static void main(String[] args){
// 创建一个 Printer 对象, 当成 Output 使用
Output o = new Printer();
o.getData("轻量级 Java EE 企业应用实战");
o.getData("疯狂 Java 讲义");
o.out();
o.getData("疯狂 Android 讲义");
o.getData("疯狂 Ajux 讲义");
o.out();
// 调用 Output 接口中定义的默认方法
o.print("孙悟空", "猪八戒", "白骨精");
o.test();
// 创建一个 Printer 对象, 当成 Product 使用
Product p = new Printer();
System.out.println(p.getProductTime());
// 所有接口类型的引用变量可直接赋给 Object 类型的变量
Object obj = p;
System.out.println(obj); // out: app.Printer@16f65612
System.out.println(obj.toString());
}
}
6.5 接口和抽象类
相同点
- 接口和抽象类都不能被实例化, 它们都位于继承树的顶端, 用于被其他类实现和继承
- 接口和抽象类都可以包含抽象方法, 实现接口或继承抽象类的普通子类都必须实现这些抽象方法
不同点
- 接口作为一种规范和实现分离的设计哲学, 对接口的实现者而言, 接口规定了实现者必须向外提供那些服务(以方法的形式提供); 对接口的调用者而言, 接口规定了调用者可以调用那些服务, 以及如何调用这些服务(就是如何来调用方法)
- 当一个程序中使用接口时, 接口是多个模块间的耦合标准; 当多个应用程序之间使用接口时, 接口是多个程序之间的通讯标准
- 抽象类是一种模版式设计方式, 抽象类作为多个子类的抽象父类, 可以被当成系统实现过程中的中间产品, 这个中间产品已经实现了系统的部分功能(那些已经实现的方法), 但这个产品依然不呢个当成最终产品, 必须有更进一步的完善, 这种完善可能有几种不同方式
接口和抽象类在用法上的不同
- 接口里只能包含抽象方法和默认方法, 不能为普通方法提供方法实现; 抽象类则完全可以包含普通方法
- 接口里不能定义静态方法; 抽象类里可以定义静态方法
- 接口里只能定义静态常量, 不能定义普通成员变量; 抽象类里则既可以定义普通成员变量, 也可以定义静态常量
- 接口里不包含构造器; 抽象类里可以包含构造器, 抽象类里的构造器并不是用于创建对象, 而是让其子类调用这些构造器来完成属于抽象类的初始化操作
- 接口里不包含初始化块; 但抽象类则完全可以包含初始化块
- 一个类最多只能有一个直接父类, 包括抽象类; 但一个类可以直接实现多个接口, 通过实现多个接口可以弥补Java多继承的不足
6.6 面向接口编程
接口体现的是规范和实现分离的设计哲学, 充分利用接口可以极好的降低程序各模块之间的耦合, 从而提高系统的可扩展性和可维护性
1. 简单工厂模式
示例代码
/**
* 需求(场景): 假设程序中有个 Computer 类需要组合一个输出设备
* 解决方案: 1.1 直接让 Computer 类组合一个 Printer
* 1.2 让 Computer 类组合一个 Output
* 分析: 工厂模式简易让 Computer 类组合一个 Output 类型的对象, 将 Computer 类与 Printer 类完全分离
*/
// 例子:
// Computer 类的定义代码
public class Computer{
private Output out;
public Computer(Output out){
this.out = out;
}
// 定义一个模拟获取字符串输入的方法
public void keyIn(String msg){
out.getData(msg);
}
// 定义一个模拟打印的方法
public void print(){
out.out();
}
}
// 程序说明:
// 1. Computer 类已经完全与 Printer 类分离, 只与 Output 接口耦合. Computer 类不再负责创建 Output 对象,
// 系统提供一个 Output 工厂来负责生成 Output 对象
// 例子2:
// 定义一个 OutputFactory 工厂类
public class OutputFactory{
public Output getOutput(){
// return new Printer();
return new BetterPrinter();
}
public static void main(String[] args){
OutputFactory of = new OutputFactory();
Computer c = new Computer(of.getOutput());
c.keyIn("轻量级 Java EE 企业应用实战");
c.keyIn("疯狂 Java 讲义");
c.print();
}
}
// 程序说明:
// 1. OutputFactory 类只包含了一个 getOutput() 方法, 该方法返回一个 Output 的实现类的实例, 该方法负责
// 创建 Output 实例, 具体创建哪一个实现类的对象由该方法决定(具体由该方法中 return 后面的实现类的实例
// 决定, 也可以增加更复杂逻辑的代码, 能力不够啊 苏沐橙)
// 例子3:
// 定义一个 BetterPrinter 类
public class BetterPrinter implements Output{
private String[] printData = new String[MAX_CACHE_LINE * 2];
// 用以记录当前需要打印的作业数
private int dataNum = 0;
public void out(){
// 只要有作业, 就继续打印
while (dataNum > 0){
System.out.println("高速打印机正在打印:" + printData[0]);
// 把作业队列整体前移一位, 并将剩下额作业数减 1
System.arraycopy(printData, 1, printData, 0, --dataNum);
}
}
public void getData(String msg){
if (dataNum >= MAX_CACHE_LINE * 2){
System.out.println("输出队列已满, 添加失败");
}
else{
// 把打印数据添加到队列里, 已保存数据的数量加1
printData[dataNum++] = msg;
}
}
}
// 程序说明:
// 1. BetterPrinter 类和 Printer 类一样, 也是实现类 Output 接口的实现类, 因此也可以当成 Output 对象
// 使用,
// 2. 通过这种方式, Computer 调用 Output 接口, 而 OutputFactory 则负责调用 Output 的实现类
// 3. 把所有生成 Output 对象的逻辑集中在 OutputFactory 工厂类中管理, 而所有需要使用 Output 对象的类只
// 需与 Output 接口耦合, 而不是与具体的实现类耦合
7. 内部类
- 内部类: 把一个类放在另一个类的内部定义, 这个定义在其他类内部的类就称为内部类(也叫嵌套类); 包含内部类的类也被称之为外部类(或者叫宿主类)
- 内部类提供了更好的封装, 可以把内部类隐藏在外部类之内, 不允许同一个包中其他类访问该类
- 内部类成员可以直接访问外部类的私有数据, 因为内部类被当成其外部类成员, 同一个类的成员之间可以互相访问; 但是外部类不能直接访问内部类的实现细节, 例如内部类的成员变量或方法
- 匿名内部类适合用于创建那些仅需要一次使用(不需要复用)的类
- 内部类比外部类可以多使用三个修饰符: private, static, protected , 外部类不可以使用这三个修饰符
- 非静态内部类不能拥有静态成员
7.1 非静态内部类
- 内部类可以定义在外部类类体的任意位置, 甚至在方法中也可以定义内部类(方法里定义的内部类叫做局部内部类)
- 大部分时候内部类都被作为成员内部类定义, 而不是局部内部类. 成员内部类是一种与成员变量, 方法, 构造器和初始化块相似的类成员; 局部内部类和匿名内部类则不是类成员
- 使用 static 修饰的成员内部类就是静态内部类. 没有使用 static 修饰的成员内部类就是非静态内部类
- 外部类对应2个作用域: 同一个包和任何位置; 对应的访问权限: 包访问权限(省略不写)和 public 访问权限; 内部类对应4个作用域: 同一个类, 同一个包, 父子类, 任何位置; 对应访问权限: private, default, protected, public
- 如果外部类成员变量,非静态内部类成员变量与非静态内部类里方法的局部变量同名, 可通过 this.varName-->代表访问内部类变量; 通过 外部类名.this.varName-->代表访问外部类变量(例子2)
- 但是外部类访问非静态内部类private属性的变量不成立, 如果要访问只能通过显式创建非静态内部类对象来调用访问,即通过:new InnerClass().varName 或 new InnerClass().method() (例子3)
- 根据静态成员不能访问非静态成员的规则,外部类的静态方法,静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量,创建实例等。总之:不允许外部类静态成员中直接使用非静态内部类(例子4)
- 非静态内部类和外部类对象关系: 非静态内部类实例必须寄生在外部类实例里, 而外部类对象则不必一定有非静态内部类对象寄生其中. 即: 如果存在一个非静态内部类对象, 则一定存在一个被它寄生的外部类对象; 但外部类对象存在时,外部类对象里不一定寄生了非静态内部类对象.
定义语法
public class OuterClass{
// 类体里面定义内部类
private InnerClass{
// 这是一个内部类
}
}
示例代码
// 例子1:
// 在程序Cow类里面定义一个CowLeg非静态内部类, 并在CowLeg类的实例方法中直接访问Cow的private访问权限的实例变量
public class Cow{
private double weight;
// 外部类的两个重载的构造器
public Cow(){}
public Cow(double weight){
this.weight = weight;
}
// 定义一个非静态的内部类
private class CowLeg{
// 非静态内部类的两个实例变量
private double length;
private String color;
// 非静态内部类的两个重载的构造器
public CowLeg(){}
public CowLeg(double length, String color){
this.length = length;
this.color = color;
}
// color 的 setter 和 getter 方法
public void setColor(String color){
this.color = color;
}
public String getColor(){
return this.color;
}
// length 的 setter 和 getter 方法
public void setLength(double length){
this.length = length;
}
public double getLength(){
return this.length;
}
// 非静态内部类的实例方法
public void info(){
System.out.println("当前牛腿颜色是:" + color + ", 高: " + length);
// 直接访问外部类的 private 访问权限的成员变量
System.out.println("这条牛腿的奶牛重: " + weight); // ①
}
public void info2(){
System.out.println("牛腿颜色是:" + getColor() + ", 长是: " + getLength());
// 直接访问外部类的 private 访问权限的成员变量
System.out.println("奶牛重: " + weight);
}
}
public void test(){
CowLeg c1 = new CowLeg(1.23, "黑白相间");
c1.info();
CowLeg c2 = new CowLeg();
c2.setColor("red");
c2.setLength(2.58);
c2.info2();
}
public static void main(String[] args) {
Cow cow = new Cow(378.9);
cow.test();
}
}
// 程序说明:
// 1. 在外部类里使用非静态内部类时, 与平时使用普通类并没有什么区别
// 2. 成员内部类的class文件形式: OuterClass$InnerClass.class
// 3. ①处的非静态内部类的实例方法中直接访问外部类的private属性的实例变量, 这是因为在非静态内部类对象里,
// 保存了一个它所寄生的外部类对象的引用(当调用非静态内部类的实例方法时, 必须有一个非静态内部类的实例,
// 非静态内部类实例必须寄生在外部类实例里)
// 4. 非静态内部类的方法访问某个变量时顺序: 方法内-->内部类-->外部类
// 例子2:
// 通过 this.变量 和 外部类.this.变量 区分内部类和外部类同名变量
public class DiscernVariable{
private String prop = "外部类的实例变量";
private class InClass{
private String prop = "内部类的实例变量";
public void info(){
String prop = "方法体局部变量";
// 通过外部类类名.this.varName 访问外部类的实例变量
System.out.println("外部类的实例变量: " + DiscernVariable.this.prop);
// 通过 this.varName 访问内部类实例变量
System.out.println("内部类的实例变量: " + this.prop);
// 直接访问局部变量
System.out.println("局部变量的值: " + prop);
}
}
public void test(){
InClass in = new InClass();
in.info();
}
public static void main(String[] args) {
new DiscernVariable().test();
}
}
// 程序说明:
// 1. 记住: 内部类通过 外部类类名.this.varName 访问外部类的实例变量
// 内部类通过 this.varName 访问内部类实例变量
// 例子3:
// 外部类访问非静态内部类的private属性的变量
public class Outer{
private int outProp = 9;
class Inner{
private int inProp = 5;
public void accessOutProp(){
// 非静态内部类可以直接访问外部类的 private 成员变量
System.out.println("外部类的outProp值: " + Outer.this.outProp);
// 因为变量不同名, 所以可以直接调用
System.out.println("外部类的outProp值: " + outProp);
}
}
public void accessInnerProp(){
// 外部类不能直接访问非静态内部类的实例变量
// 下面代码出现编译错误
// System.out.println("内部类的 inProp值: " + inProp);
// 如需访问内部类的实例变量, 必须显式创建内部类对象
System.out.println("内部类的 inProp值: " + new Inner().inProp);
}
public static void main(String[] args){
// 执行下面代码, 只创建外部对象, 还未创建内部类对象
Outer out = new Outer();
// 调用外部类的方法
out.accessInnerProp();
// 外部类实例调用内部类的属性和方法
System.out.println("外部类通过内部类实例调用内部类private变量inProp: " + out.new Inner().inProp);
// 外部类实例调用内部类的属性和方法
// 调用内部类的方法 记住这种方法 vscode 插件提醒的 哈哈哈哈
out.new Inner().accessOutProp();
}
}
// 程序说明:
// 1. main 方法中创建了一个外部类的对象, 并调用了外部类对象的accessInnerProp()方法, 此时非静态内部类对象
// 根本不存在,所以为了调用就必须通过创建内部类实例的形式来调用
// 2. 说明: 从外部类调用内部类的变量或方法, 只能通过 外部类实例. new 内部类().变量
// 或者: 外部类实例. new 内部类().方法 这种形式调用内部类的属性或方法
// 例子4:
// 外部类的静态成员不能访问非静态内部类
public class StaticTest{
// 定义一个非静态的内部类,是一个空类
private class In{}
// 外部类的 静态方法
public static void main(String[] args){
// 下面的代码引发异常, 因为 main() 是静态成员
// 无法访问非静态成员 In 类
// new In();
}
}
// Java 不允许非静态内部类定义静态成员,下面代码错误示范
public class InnerNoStatic{
private class InnerClass{
/*
* 下面三个静态声明都将引发编译异常
* 非静态内部类不能有静态声明
*/
// 静态初始化块
static {
System.out.println("========");
}
// 静态成员变量和静态方法
private static int inProp;
private static void test(){}
}
}
7.2 静态内部类
- 使用 static 修饰的内部类被称为类内部类,或叫静态内部类;因为 static 修饰的内部类属于外部类本身,而不属于外部类的某个对象
- 静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。 即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员(例子1)
- 静态内部类是外部类的一个静态成员,因此外部类的所有方法,所有初始化块中可以使用静态内部类来定义变量,创建对象
- 外部类不能直接访问静态内部类的成员,但可以使用静态内部类的类名作调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员 (例子2)
- 也可以在Java接口里定义内部类,接口里定义内部类默认使用 public static 修饰,即:接口里的内部类都是静态的
- 静态内部类的实例方法也不能访问外部类的实例属性: 原因: 因为静态内部类是外部类的类相关, 而不是外部类的对象相关; 即:静态内部类对象不是寄生在外部类的实例中, 而是寄生在外部类本身中. 当静态内部类对象存在时, 并不存在一个被它寄生的外部类对象, 静态内部类对象只持有外部类的引用, 没有持有外部类对象的引用.
示例代码
// 例子1:
public class StaticInnerClassTest{
private int prop1 = 5;
private static int prop2 = 9;
// 定义一个静态内部类
static class StaticInnerClass{
public void accessOutProp(){
// 下面的代码出现错误:静态内部类无法访问外部类的实例变量
System.out.println("prop1");
// 静态成员只可以访问静态的
System.out.println("prop2");
}
}
}
// 例子2:
public class AccessStaticInnerClass{
static class StaticInnerClass{
private static int prop1 = 5;
private int prop2 = 9;
}
public void accessInnerProp(){
// 外部类访问静态内部类类变量
// System.out.println(prop1);
// 上面的代码出错,应改写为如下形式
// 通过类名访问静态内部类的类成员
System.out.println(StaticInnerClass.prop1);
// 外部类访问静态内部类实例变量
// System.out.println(prop2);
// 上面的代码出错,应改写为如下形式
// 通过类名访问静态内部类的实例成员
System.out.println(new StaticInnerClass().prop2);
}
}
// 程序说明:
// 1. 记住规则:静态成员不能访问非静态成员
// 2. 外部类调用内部类成员:使用内部类名调用类成员,使用内部类的实例调用实例成员
7.3 使用内部类
-
在外部类内部使用内部类
- 可以直接通过内部类名来定义变量,或通过 new 调用内部类构造器来创建实例
- 和普通类区别:不要再外部类的静态成员(静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员 again
-
外部类以外使用非静态内部类
- 如果满足标题所说在外部类以外的地方访问内部类,那么内部类不能使用 private 修饰;省略控制符代表只能被外部类同包的类使用;protected 控制符的内部类表示;可被与外部类处于同一个包中的其他类和外部类的子类访问;public控制符的内部类表示:可在任何地方被访问
-
外部类以外的地方使用内部类(静态非静态两种)变量的语法
OuterClass.InnerClass varName
// 说明:
// 在外部类以外的地方使用内部类时,内部类完整的类名应该是 OuterClass.InnerClass, 如果外部类有包名,还应加
// 上包名前缀
- 外部类以外的地方创建非静态内部类实例的语法
OuterInstance.new InnerConstrctor()
// 说明:
// 1. 由于非静态内部类的对象必须寄生在外部类的对象里,因此创建非静态内部类对象之前,必须先创建其外部类对象
// 2. 在外部类以外的地方创建非静态内部类实例必须使用外部列实例和new来调用非静态内部类的构造器(例子1)
// 3. 如果需要在外部类以外的地方创建非静态内部类的子类,注意:非静态内部类的构造器必须通过外部类的对象来调用(例子2)
- 示例代码
// 例子1:
class Out{
// 定义一个内部类, 不使用访问控制符(属于默认包访问权限)
class In{
// 内部类的构造器
public In(String msg){
System.out.println(msg);
}
}
}
public class CreateInnerInstance{
public static void main(String[] args){
// 定义非静态内部类的实例(通过外部类的实例调用)
Out.In in = new Out().new In("测试信息");
/*
* 上面的代码可改为如下三行代码
* 使用 OutterClass.InnerClass 的形式定义内部变量
* Out.In in;
* 创建外部实例, 非静态内部类实例将寄生在该实例中
* Out out = new Out();
* 通过外部类实例和new来调用内部类构造器创建非静态内部类实例
* in = out.new In("测试信息");
*/
}
}
// 程序说明:
// 1.非静态内部类的构造器必须使用外部类对象来调用
// 例子2:
public class SubClass extends Out.In{
// 显式定义 SubClass 的构造器
public SubClass(Out out){
// 通过传入的 Out 对象显式调用 In 的构造器
out.super("通过传入的 Out 对象显式调用 In 的构造器");
}
}
// 程序说明:
// 1. 当创建一个子类时, 子类构造器总会调用父类的构造器, 因此在创建非静态内部类的子类时, 必须保证让子类构造器
// 可以调用非静态内部类的构造器
// 2. 调用非静态内部类的构造器时, 必须存在一个外部类对象
// 3. 非静态内部类 In 类的构造器必须使用外部类对象来调用, 代码中 super 代表调用 In 类的构造器, 而 out 则代
// 表外部类对象; 如果需要创建 SubClass 对象时, 必须先创建一个 Out 对象, 因为 SubClass 是非静态内部类
// In 类的子类, 非静态内部类 In 对象里必须有一个对 Out 对象的引用, 其子类 SubClass 对象里也应该持有Out
// 对象的引用, 当创建 SubClass 对象时传给该构造器的 Out 对象, 就是 SubClass 对象里 Out 对象引用所指向
// 的对象
// 4. 非静态内部类 In 对象和 SubClass 对象都必须持有指向 Out 对象的引用, 区别是创建两种对象时传入的 Out 对象
// 的方式不同: 当创建非静态内部类 In 类的对象时, 必须通过 Out 对象来调用new关键字; 而当创建 SubClass 对
// 象时, 必须使用 Out 对象作为调用者来调用 In 类的构造器
// 5. 非静态内部类的子类不一定是内部类,它可以是一个外部类. 但非静态内部类的子类实例一样需要保留一个引用指向父
// 类所在的外部类对象
- 在外部类以外创建静态内部类实例语法
new OuterClass.InnerConstrctor()
// 语法说明:
// 1. 因为静态内部类是外部类类相关, 因此创建静态内部类对象时无需创建外部类对象
// 2. 不管是静态内部类还是非静态内部类,他们的声明变量的语法完全一样, 区别在于创建内部类对象时, 静态内部类只需要
// 使用外部类即可调用构造器; 而非静态内部类必须使用外部类的对象来调用构造器
- 创建静态内部类子类的语法
public class StaticSubClass extends StaticOut.StaticIn{}
- 示例代码
// 例子:
class StaticOut{
// 定义一个静态内部类, 不使用访问控制符(默认包访问权限)
static class StaticIn{
public StaticIn{
System.out.println("静态内部类的构造器");
}
}
}
public class CreateStaticInnerInstance{
public static void main(String[] args){
// 定义一个静态内部类实例
StaticOut.StaticIn in = new StaticOut.StaticIn();
/*
* 上面的代码可改为如下两行代码
* 使用 OutterClass.InnerClass 的形式定义内部变量
* StaticOut.StaticIn in;
* 通过 new 来调用内部类构造器创建静态内部类实例
* in = new StaticOut.StaticIn();
*/
}
}
-
局部内部类(了解就好了, 作用域太小, 很少使用)
- 定义在方法里面的类就是局部内部类, 局部内部类仅在该方法里有效
- 由于局部内部类不能在外部类的方法以外的地方使用, 因此局部内部类也不能使用访问控制符和static修饰符
- 只能在局部内部类方法中进行定义变量, 创建实例或派生子类
-
示例代码
// 例子:
public class LocalInnerclass{
public static void main(String[] args){
// 定义局部内部类
class InnerBase{
int a;
}
// 定义局部内部类的子类
class InnerSub extends InnerBase{
int b;
}
// 创建局部内部类的对象
InnerSub is = new InnerSub();
is.a = 5;
is.b = 8;
System.out.println("InnerSub对象的 a 和 b 实例变量是: " + is.a + ", " + is.b);
}
}
// 程序说明:
// 1. 局部内部类的class文件的文件名比成员内部类的 class 文件的文件名多了一个数字, 这是因为同一个类里面
// 不可能有两个同名的成员内部类, 而同一个类里则可能有两个以上同名的局部内部类
7.5 Java8改进的匿名内部类
- 匿名内部类适合创建那种只需要一次使用的类, 创建匿名内部类时会立即创建一个该类的实例, 这个类定义立即消失匿名内部类不能重复使用
- 最常用的创建匿名内部类的方式是需要创建某个接口类型的对象(例子1)
- 当通过实现接口来创建匿名内部类时, 匿名内部类也不能显式的创建构造器, 因此匿名内部类只有一个隐式的无参数构造器, 所以 new 接口名后面的括号里不能传入参数值
- 如果通过继承父类来创建匿名内部类时, 匿名内部类将拥有和父类相似的构造器, 相似指的是: 相同形参列表
- 当创建匿名内部类时, 必须实现接口或抽象父类里的所有抽象方法, 也可以重写父类中的普通方法
- 匿名内部类定义语法
new 实现接口() | 父类构造器(实参列表){
// 匿名内部类的类体部分
}
-
定义匿名内部类注意规则
- 匿名内部类不能是抽象类, 因为系统在创建匿名内部类时, 会立即创建匿名内部类对象, (抽象类不能创建实例)
- 匿名内部类不能定义构造器. 由于匿名内部类没有类名. 所以无法定义构造器, 但匿名内部类可以定义初始化块, 可以通过实例初始化块来完成构造器需要完成的事情
-
示例代码
// 例子1:
// 最常用的创建匿名内部类的方式是需要创建某个接口类型的对象
interface Product{
public double getPrice();
public String getName();
}
public class AnonymousTest{
public void test(Product p){
System.out.println("购买了一个" + p.getName() + ",花掉了" + p.getName());
}
public static void main(String[] args){
AnonymousTest ta = new AnonymousTest();
// 调用 test() 方法时, 需要传入一个 Product 参数
// 此处传入其匿名实现类的实例 ①
ta.test(new Product()
// 匿名类类体部分
{
// 实现Product接口类的两个方法
public double getPrice(){
return 567.8;
}
public String getName(){
return "AGP显卡";
}
});
// 调用例子2创建的Product接口的独立实现类的实例
// ta.test(new AnonymousProduct());
}
}
// 程序说明:
// 1. AnonymousTest类定义了一个test()方法, 该方法需要一个 Product 对象作为参数, 但 Product 只是一个接口,
// 无法直接创建对象, 因此这里考虑创建一个 Product 接口实现类的对象传入给test()方法; 如果这个 Product
// 接口实现类需要重复使用, 则应该将该实现类定义层一个独立的类(例子2); 如果这个Product 接口实现类只需要
// 使用一次, 则可以使用匿名内部类的方式(例子1 ①处的代码)
// 例子2:
class AnonymousProduct implements Product{
// 实现Product接口类的两个方法
public double getPrice(){
return 567.8;
}
public String getName(){
return "AGP显卡";
}
}
8. Java8新增的Lambda表达式
- Lambda 表达式支持将代码块作为方法的参数, Lambda 表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称作函数式接口)的实例
- 所有的 Lambda 的类型都是一个接口, 而 Lambda 表达式本身, 也就是"代码块"需要是这个接口的实现; 即:Lambda 表达式本身就是一个函数式接口的实现
8.1 Lambda 表达式入门
- Lambda 表达式的主要作用就是代替匿名内部类的繁琐语法
Lambda 表达式组成
- 形参列表. 形参列表允许省略形参类型. 如果形参列表中只有一个参数, 甚至形参列表的的圆括号也可以省略
- 箭头(->). 必须通过英文中画线号和大于符号组成
- 代码块. 如果代码块只包含一条语句, Lambda 表达式允许省略代码块的花括号, 那么这条语句就不要用花括号表示语句结束.
- Lambda 代码块只有一条 return 语句, 甚至可以省略 return 关键字
- Lambda 表达式需要返回值, 而它的代码块中仅有一条省略了return语句, Lambda表达式会自动返回这条语句的值
示例代码
// 例子1:
// Lambda 表达式的初级用法
interface Eatable{
void taste();
}
interface Flyable{
void fly(String weather);
}
interface Addable{
int add(int a, int b);
}
public class LambdaQs{
// 调用该方法需要 Eatable 对象
public void eat(Eatable e){
System.out.println(e);
e.taste();
}
// 调用该方法需要 Flyable 对象
public void drive(Flyable f){
System.out.println("我正在驾驶: " + f);
f.fly("碧空如洗的晴日");
}
// 调用该方法需要 Addable 对象
public void test(Addable add){
System.out.println("5 与 3 的和为: " + add.add(5,3));
}
public static void main(String[] args) {
LambdaQs lq = new LambdaQs();
// Lambda 表达式的代码块只有一条语句, 可以省略花括号
lq.eat(() -> System.out.println("苹果的味道不错的哈!")); // ①
// lambda表达式的形参列表只有一个形参, 可以省略圆括号
lq.drive(weather -> { // ②
System.out.println("今天天气是: " + weather);
System.out.println("直升机飞行平稳");
});
// lambda 表达式的代码块只有一条语句, 可以省略花括号
// 代码块中只有一天语句, 即使该表达式需要返回值, 也可以省略 return 关键字
lq.test((a, b) -> a + b); // ③
}
}
// 程序说明:
// 1. ① 处的代码调用eat()方法, 而eat()方法接收一个 Eatable 类型的参数(即 Eatable 接口的实现类), 实际用的是
// Lambda 表达式, 也可以使用匿名内部类
// 2. ② 处 ③ 处出的代码同一处的代码说明
8.2 Lambda表达式和函数式接口
- Lambda 表达式的目标类型必须是"函数式接口(functional interface)". 函数式接口代表只包含一个抽象方法的接口,函数式接口可以包含多个默认方法(default 修饰), 类方法, 但是只能声明一个抽象方法
- 如果采用匿名内部类语法类创建函数式接口的实例, 则只需要实现一个抽象方法, 在这种情况下即可采用 Lambda 表达式来创建对象, 该表达式创建出来的对象的目标类型就是这个函数式接口
- java8 专门为函数式接口提供 @functionalInterface 注解, 通常放在接口定义前面(类似python的装饰器), 该注解对程序功能没有任何作用, 只要用于告诉编译器执行更严格的检查=--检查该接口必须是函数式接口, 否则报错
- 由于Lambda表达式的结果就是被当成对象, 因此程序中完全可以使用Lambda表达式进行赋值(例子1)
- Lambda 表达式的目标类型完全可能是变化的---唯一要求是: Lambda表达式实现的匿名方法与目标类型(函数式接口)中唯一的抽象方法有相同的形参列表(例子3)
Lambda 表达式的限制
- Lambda 表达式的目标类型必须是明确的函数式接口
- Lambda 表达式只能为函数式接口创建对象. Lambda 表达式只能实现一个方法, 因此它只能为只有一个抽象方法的接口(函数式接口) 创建对象
保证 Lambda 表达式目标类型为函数式接口的方式
- 将 Lambda 表达式赋值给函数式接口类型的变量(例子1)
- 将 Lambda 表达式作为函数式接口类型的参数传给某个方法
- 使用函数式接口对 Lambda 表达式进行强制类型转换(例子2)
java8中java.util.function 包下预定义的函数式接口
- XxxFunction:
这类接口中通常包含一个 apply() 抽象方法, 该方法对参数进行处理,转换(apply()方法的处理逻辑由Lambda表达式来实现), 然后返回一个新的值; 该函数式接口通常用于对指定数据转换处理 - XxxConsumer:
这类接口中通常包含一个 accept() 抽象方法, 该方法与 XxxFunction 接口中 apply() 方法基本相似, 也负责对参数进行处理, 只是该方法不会返回处理结果 - XxxPredicate:
这类接口中通常包含一个test()抽象方法, 该方法通常用来对参数进行某种判断(test()方法的处理逻辑由Lambda表达式来实现), 然后返回一个boolean 值 - XxxSupplier:
这类接口中通常包含一个 getAsXxx()抽象方法, 该方法不需要输入参数,该方法会按照某种逻辑算法(getAsXxx()方法的处理逻辑由Lambda表达式来实现)返回一个数据
示例代码
// 例子1:
// Runnable 接口中只包含一个无参数的方法
// Lambda 表达式代表匿名方法实现了 Runnable 接口中唯一的, 无参数的方法
// 因此下面的Lambda 表达式创建了一个 Runnable 对象
Runnable r = () -> {
for (int i = 0; i < 100; i++){
System.out.println(i);
}
};
// 例子2:
// 错误的代码将Lambda表达式赋给一个引用类型变量(必须赋给函数式接口类型变量)
Object obj = () -> {
for (int i = 0; i < 100; i++){
System.out.println(i);
}
};
// 报错: 不兼容类型: Object 不是函数式接口
// 通过对 Lambda 表达式执行强制类型转换, 表明该表达式的目标类型为 Runnable 函数式接口
Object obj = (Runnable)() -> {
for (int i = 0; i < 100; i++){
System.out.println(i);
}
};
// 例子3:
// 函数式接口FKTest中仅定义了一个不带参数的方法, FKTest接口中唯一的抽象方法是不带参数的
@functionalInterface
interface FKTest{
void run();
}
// 同样的 Lambda 表达式可以被当成不同的目标了类型, 唯一的要求是
// Lambda 表达式的形参列表与函数式接口中唯一的抽象方法的形参列表相同
object obj2 = (FKTest)() -> {
for (int i = 0; i < 100; i++){
System.out.println(i);
}
};
8.3 方法引用与构造器引用
- 如果Lambda表达式的代码块只有一条代码, 可以在代码块中使用方法引用和构造器引用
Lambda表达式支持的方法引用和构造器引用
种类 | 示例 | 对应Lambda表达式 | 说明 |
---|---|---|---|
引用类方法 | 类名::类方法 | (a,b,...)-> 类名.类方法(a,b,...) | 函数式接口中被实现方法的全部参数传给该类方法作为参数 |
引用特定对象的实例方法 | 特定对象::实例方法 | (a,b,...)-> 特定对象.实例方法(a,b,...) | 函数式接口中被实现方法的全部参数传给该方法作为参数 |
引用某类对象的实例方法 | 类名::实例方法 | (a,b,...)-> a.实例方法(b,...) | 函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数 |
引用构造器 | 类名::new | (a,b,...)-> new 类名(a,b,...) | 函数式接口中被实现方法的全部参数传给该构造器作为参数 |
8.3.1 引用类方法
// 例子:
@functionalInterface
interface Converter{
Integer convert(String from);
}
// 程序说明:
// 1. 该函数式接口中包含一个convert()抽象方法, 该方法负责将 String 参数转换为 Integer
- 使用Lambda表达式创建一个Converter对象
// 例子1:
// 下面的代码使用 Lambda 表达式创建 Converter 对象
Converter converter1 = from -> Integer.valueOf(from);
//
// 例子2:
// 调用 converter1 对象的 convert()方法将字符串转换为数字
Integer va1 = converter1.convert("99");
System.out.println(va1) // out: 99
// 代码调用 converter1 对象的 conver()方法时, 由于 converter1 对象是 Lambda 表达式创建的, convert()
// 方法执行体就是Lambda表达式的代码块部分
//
// 例子3:
// 上面的Lambda表达式的代码块只有一行调用类方法的代码块, 因此例子2也可以使用例子3的的形式替换
// 方法引用代替Lambda表达式: 引用类方法
// 函数式接口中被实现方法的全部参数传给类方法作为参数
Converter converter1 = Integer::valueOf;
// 程序说明:
// 1. 对于上面的类方法引用, 也就是调用Integer类的valueOf()类方法来实现Converter函数式接口中唯一的
// 抽象方法, 当调用 Converter 接口中的唯一的抽象方法时, 调用的参数将会传给 Integer类的valueOf()
// 类方法
8.3.2 引用特定对象的实例方法
- 使用Lambda表达式创建一个Converter对象
// 下面的代码使用 Lambda 表达式创建一个Converter对象
Converter converter2 = from -> "fkit.org".indexOf(from);
// 说明:1. 上面Lambda表达式的代码块只有一条语句, 因此程序省略了该代码块的花括号, 而且表达式所实现的
// convert()方法需要返回值, 因此Lambda表达式将会把这条代码的值作为返回值
- 调用 converter1 对象的 convert()方法件字符串转换为整数
Integer value = converter2.convert("it");
System.out.println(value) // out: 2
- 使用 Lambda 表达式来替换
// 方法引用替换 Lambda 表达式: 引用特定对象的实例方法
// 函数式接口中被实现方法的全部参数传给该方法最为参数
Converter converter2 = "fkit.org"::indexOf;
// 说明:1. 调用"fkit.org"对象的 indexOf()实例方法来实现Converter 函数式接口中唯一的抽象方法
8.3.3 引用某类对象的实例方法
// 1. 看例子来理解
// 例子:
// 函数式接口
@functionalInterface
interface MyTest{
// 抽象方法
String test(String a, int b, int c);
}
//
// 使用Lambda表达式来创建MyTest对象
// 下面的代码使用Lambda表达式来创建MyTest对象
MyTest mt = (a, b, c) -> a.substring(b, c);
//
// 调用 mt 对象的test()方法
String str = mt.test("I Love Java", 2, 9);
System.out.println(str); // out: Love Ja
//
// 使用如下的方法来替换上面的程序
// 方法引用代替Lambda表达式:引用某个对象的实例方法
// 函数式接口中被实现方法的第一个参数作为调用者
// 后面的参数全部传给该方法作为参数
MyTest mt = String::substring;
// 说明:
// 1. 对于上面的实例方法引用, 也就是调用某个String对象的 subdstring()实例方法来实现 MyTest 函数
// 式接口中的唯一的抽象方法, 当调用 MyTest 接口中的唯一的抽象方法时, 第一个调用参数将作为
// subdstring()方法的调用者, 剩下的调用参数会作为 subdstring()实例方法的调用参数
8.3.4引用构造器
// 1. 就看例子吧, 反正看不懂
// 例子:
// 定义函数式接口
@functionalInterface
interface YourTest{
JFrame win(String title);
}
//
// 使用Lambda表达式创建对象
YourTest yt = (String a) -> new JFrame(a);
//
// 调用 yt 对象的 win() 方法
JFrame jf = yt.win("我的窗口");
System.out.println(jf);
//
// 构造器引用代替Lambda表达式
// 函数式接口中被实现方法的全部参数传给该构造器作为参数
YourTest yt = JFrame::new;
8.4 lambda 表达式与匿名内部类的联系和区别
相同点
- Lambda 表达式与匿名内部类一样, 都可以直接访问 "effectively final" 的局部变量, 以及外部类的成员变量(包括类变量和实例变量)(例子1)
- Lambda 表达式创建的对象与匿名内部类生成的对象一样, 都可以直接调用从接口继承的默认方法
不同点
- 匿名内部类可以为任意接口创建实例--不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但是Lambda表达式只能为函数式接口创建实例
- 匿名内部类可以为抽象类甚至普通类创建实例; 但是Lambda表达式只能为函数式接口创建实例
- 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法; 但是Lambda表达式的代码块不允许调用接口中定义的默认方法
示例代码
// 例子1:
@FunctionalInterface
interface Displayable{
// 定义一个抽象方法和默认方法
void display();
default int add(int a, int b){
return a + b;
}
}
public class LambdaAndInner{
private int age = 12;
private static String name = "疯狂软件教育中心";
public void test(){
String book = "疯狂Java讲义";
Displayable dis = () -> {
// 访问 "effectively final " 的局部变量
System.out.println("book局部变量为:" + book); // ##
// 访问外部类的实例变量和类变量
System.out.println("外部类的age实例变量为:" + age); // ##
System.out.println("外部类的 name 类变量为:" + name); // ##
};
dis.display();
// 调用 dis 对象从接口中继承的 add() 方法
System.out.println(dis.add(3, 5));
}
public static void main(String[] args) {
LambdaAndInner lambda = new LambdaAndInner();
lambda.test();
}
}
// 程序说明:
// 1. 上面程序使用 Lambda 表达式创建了一个Displayable 的对象, Lambda 表达式的代码块中的 ## 代码分别
// 表示访问了 "effectively final" 的局部变量, 外部类的实例变量好类变量
// 2. 由于Lambda 表达式访问了 book 局部变量, 因此该局部变量相当于有一个隐式的 final 修饰, 因此不允许
// 对 book 局部变量重新赋值
// 3. 虽然Lambda表达式的目标类型: Displayable 中包含了 add()方法, 但Lambda表达式的代码块不允许调用
// 这个方法; 如果将上面的Lambda表达式改为匿名内部类的写法, 当匿名内部类实现display()抽象方法时,
// 则完全可以调用这个 add() 方法
8.5 使用 Lambda 表达式调用 Arrays 的类方法
Arrays 类中的有些方法需要 Comparator, XxxOperator, XxxFunction 等接口的实例, 这些接口都是函数式接口, 因此可以使用Lambda表达式来调用Arrays的方法
示例代码
// 例子:
import java.util.Arrays;
public class LambdaArrays {
public static void main(String[] args) {
String[] arr1 = new String[]{"java", "fkava", "fkit", "ios", "android"};
Arrays.parallelSort(arr1, (o1, o2) -> o1.length() - o2.length());
// out: [ios, java, fkit, fkava, android]
System.out.println(Arrays.toString(arr1));
int[] arr2 = new int[]{3, -4, 25, 16, 30, 18};
// left 代表数组中前一个索引处的元素, 计算第一个元素时, left为1
// right 代表数组中当前索引处的元素
Arrays.parallelPrefix(arr2, (left, right) -> left*right);
// out: [3, -12, -300, -4800, -144000, -2592000]
System.out.println(Arrays.toString(arr2));
long[] arr3 = new long[5];
// operand 代表正在计算的元素索引
Arrays.parallelSetAll(arr3, operand -> operand * 5);
// out: [0, 5, 10, 15, 20]
System.out.println(Arrays.toString(arr3));
}
}
// 程序说明:
// 1. (o1, o2) -> o1.length() - o2.length(); 这段Lambda 表达式的目标类型是 Comparator, 该 Comparator
// 指定了判断字符串大小的标准:字符串越长, 即可认为该字符串越大
// 2. (left, right) -> left*right); 这段 Lambda表达式的目标类型是 IntBinaryOperator, 该对象将会根据
// 后两个元素来计算当前元素的值
// 3. operand -> operand * 5; 这段Lambda表达式的目标类型是 IntToLongFunction 该对象将会根据元素的索引
// 来计算当前元素的值
9. 枚举类
9.1 手动实现枚举类
- 设计方式:(通过定义类的方式实现)
- 通过 private 将构造器隐藏起来
- 把这个类的所有可能实例都使用 public static final 修饰的类变量来保存
- 如果必要, 可以提供一些静态方法, 允许其他程序根据特定的参数来获取与之匹配的实例
- 使用枚举可以使程程序更加健壮,避免创建对象的随意性
9.2 枚举类入门
- Java 5 新增了 enum 关键字(与 class interface关键字的地位相同), 用于定义枚举类
- 枚举类作为一种特殊的类, 它也可以有自己的成员变量, 方法, 也可以实现一个或多个接口, 也可以定义自己的构造器
- 一个 Java 源文件当中最多只能定义一个 public 访问权限的枚举类, 且该Java源文件也必须和该枚举类的类名相同
枚举类和普通类的区别
- 枚举类可以实现一个或多个接口, 使用 enum 定义的枚举类默认继承了 java.lang.Enum 类, 而不是默认继承 Object 类, 因此枚举类不能显式继承其他父类. 其中 java.lang.Enum 类实现了 java.lang.Serializable 和 java.lang.Comparable 两个接口
- 使用 enum 定义, 非抽象的枚举类默认会使用 final 修饰, 因此枚举类不能派生子类
- 枚举类的构造器只能使用 private 访问控制符, 如果省略了构造器的访问控制符, 则默认使用 private 修饰; 如果强制指定访问控制符, 则只能指定 private 修饰符
- 枚举类的所有实例必须在枚举类的第一行显式列出, 否则这个枚举类永远不能产生实例. 列出这些实例时, 系统会自动添加 public static final 修饰
- 枚举类默认提供了一个 values()的方法, 用于遍历所有的枚举值
- 通过 EnumClass.variable 的形式使用某个枚举类的实例
示例代码
// 例子1:
// SeasonEnum.java
// 定义一个枚举类
public enum SeasonEnum{
// 在第一行列出4个枚举实例
// 注意: 实例之间用逗号 (,) 隔开, 最后一个实例分号 (;) 结尾
SPRING, SUMMER, FALL, WINTER;
}
// 例子2:
// EnumTest.java
// 定义一个普通类使用上面的枚举类实例
public class EunmTest{
// 定义一个judge实例方法,需要传入的参数是 SeasonEnum 枚举类的实例
public void judge(SeasonEnum s){
switch (s){
case SPRING:
System.out.println("Spring is comming, I can see it!");
break;
case SUMMER:
System.out.println("Summer is comming, I want to Swimming!");
break;
case FALL:
System.out.println("Fall is comming, I can felling it!");
break;
case WINTER:
System.out.println("WInter is comming");
break;
}
}
public static void main(String[] args){
// 枚举类默认有一个 values()方法, 返回该枚举类的所有实例
for (SeasonEnum s: SeasonEnum.values()){
System.out.println(s);
}
// 使用枚举实例时, 可通过Enumclass.variable 形式来访问
new EnumTest().judge(SeasonEnum.SPRING);
}
}
java.lang.Enum 类中的一些方法
- int compareTo(E o)
说明:该方法用于指定枚举对象比较顺序, 同一个枚举实例只能与相同类型的枚举实例进行比较; 如果该枚举对象位于指定枚举对象之后, 则返回正整数; 如果该枚举对象位于指定枚举对象之前, 则返回负整数, 否则返回零 - String name()
说明:返回此枚举实例的名称, 这个名称就是定义枚举类时列出的所有枚举值之一. 与次方法相比, 大多数程序员应该优先考虑使用 toString() 方法, 因为 toSring() 方法返回更加用户友好的名称 - int ordinal()
说明:返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置, 第一个枚举值的索引为零) - String toString()
说明:返回枚举常量的名称, 与name方法此相似, 但 toString()方法更常用 - public static <T extends Enum
>T valueOf(Class enumType, String name)
说明:这是一个静态方法, 用于返回指定枚举类中指定名称的枚举值. 名称必须与该枚举类中声明枚举值时所用的标识符完全匹配, 不允许使用额外的空白字符
9.3 枚举类的成员变量, 方法和构造器
- 枚举类也是一种特殊的类, 因此它一样可以定义成员变量, 方法和构造器
- 枚举类的实例只能是枚举值, 而不是随意地通过new来创建枚举类对象
- 枚举类通常应该设计成不可变类, 即: 它的成员变量值不应该允许改变; 因此 枚举类的成员变量都应该使用 private final 修饰
- 如果将所有的成员变量都使用了 final 修饰符来修饰, 就必须在构造器里为这些成员变量指定初始值(或者在定义成员变量的时指定默认值, 或者在初始化块中指定初始值)
- 一旦为枚举类显式定义了带有参数的构造器, 列出枚举值(就是枚举类第一行列出的枚举实例)时就必须对应地传入参数
示例代码
// 例子:
// Gender.java
public enum Gender{
// 定义枚举的实例
MALE, FEMALE;
// 定义一个 public 修饰的实例变量
public String name;
}
//
// GenderTest.java
// 这个程序的不好之处在于, 直接为枚举值赋值
public class GenderTest{
public static void main(String[] args){
// 通过 Enum 的 valueOf()方法来获取指定枚举类的枚举值
Gender g = Enum.valueOf(Gender.class, "FEMALE");
// 直接为枚举值的 name 实例变量赋值
g.name = "女";
// 直接访问枚举值的name实例变量
System.out.println(g + " 代表: " + g.name);
}
}
//
// Gender.java
// 重构Gender.java程序, 设置private访问权限的实例变量, 并未变量设置 setter 和 getter 方法
public enum Gender{
MALE, FEMALE;
//
// 定义private访问权限的实例变量
private String name;
// 定义 setter 和 getter 方法
public void setName(String name){
switch (this){
case MALE:
if (name.equals("男")){
this.name = name;
}
else{
System.out.println("参数错误!");
return;
}
break;
case FEMALE:
if (name.equals("女")){
this.name = name;
}
else{
System.out.println("参数错误!");
return;
}
break;
}
}
public String getName(){
return this.name;
}
}
// GenderTest.java
// 重写 GenderTest.java 程序,
public class GenderTest{
public static void main(String[] args){
Gender g = Gender.valueOf("FEMALE");
g.setName("女");
System.out.println(g + " 代表: " + g.getName());
// 此时设置name值时将会提示参数错误
// g.setName("男");
System.out.println(g + " 代表: " + g.getName());
}
}
// Gender.java
// 重写Gender.java 代码, Gender 的实例是带参数的
public enum Gender{
// 此处的枚举值必须调用对应的构造器来创建
MALE("男"), FEMALE("女");
private final String name;
// 枚举类的构造器只能使用 private 修饰
private Gender(String name){
this.name = name;
}
public String getName(){
return this.name;
}
}
// 程序说明:
// 1. 当为Gender枚举类创建一个Gender(String name)构造器之后, 列出枚举值就应该采用 MALE("男"), FEMALE("女");
// 这种代码来完成. 也就是说, 在枚举类列出枚举值时, 实际上就是调用构造器创建枚举类对象, 只是这里无须使用 new 关键
// 字, 也无须显式调用构造器, 前面列出枚举值时无需传入参数, 甚至无需使用括号, 仅仅是因为前面的枚举类包含无参数的
// 构造器
9.4 实现接口的枚举类
- 枚举类也可以实现一个或多个接口, 与普通类实现一个或多个接口完全一样, 枚举类实现一个或多个接口时, 也需要实现该接口所包含的所有方法(使用 implements )(例子1)
- 由枚举类实现接口里的方法, 则每个枚举值在调用该方法时都有相同的行为方式(因为方法体一样)
- 如果需要让每个枚举值在调用该方法时呈现出不同的行为方式, 则可以让每个枚举值分别来实现该方法, 每个枚举值提供不同的实现方式, 从而让不同的枚举值调用该方法时具有不同的行为方式(例子2)
- 并不是所有的枚举类都是用 final 修饰, 非抽象的枚举类才默认使用 final 修饰, 对于一个抽象的枚举类而言 -- 只要它包含了抽象方法, 他就是抽象枚举类, 系统会默认使用 abstract 修饰, 而不是final修饰
示例代码
// 例子1:
// 定义一个接口(是函数式接口)
// GenderDesc.java
public interface GenderDesc{
// 定义一个抽象方法
void info();
}
//
public enum Gender implements GenderDesc{
// 此处的枚举值必须调用对应的构造器来创建
MALE("男"), FEMALE("女");
private final String name;
// 枚举类的构造器只能使用 private 修饰
private Gender(String name){
this.name = name;
}
public String getName(){
return this.name;
}
// 增加下面的 info()方法, 实现GenderDesc接口必须实现的方法
public void info(){
System.out.println("这是一个用于定义性别的枚举类");
}
}
// 例子2:
// 实现GenderDesc接口的类对与不同的枚举值调用不同的方法
public class Gender implements GenderDesc{
// 此处的枚举值必须调用对应的构造器来创建
MALE("男"){
// 花括号这部分实际上是一个类体部分
public void info(){
System.out.println("这个枚举值代表男性");
}
},
FEMALE("女"){
public void info(){
System.out.println("这个枚举值代表女性");
}
}; // 这里记住是逗号 因为本质上还是枚举类的实例, 只是提供了info()方法
private final String name;
// 枚举类的构造器只能使用 private 修饰
private Gender(String name){
this.name = name;
}
public String getName(){
return this.name;
}
}
// 程序说明:
// 1. 当创建 MALE 和 FEMALE 两个枚举值时, 后面又紧跟了一对花括号, 这对花括号里面包含了一个 info()方法定义,
// 2. 花括号部分实际上就是一个类体部分, 这种情况下, 当创建 MALE, FEMALE枚举值时, 并不是直接创建Gender枚举
// 类的实例, 而是相当于创建Gender的匿名子类的实例.
// 3. MALE 和 FEMALE 以及后面的花括号部分实际上是匿名内部类的类体部分, 所以这个部分的代码语法与前面学的匿名
// 内部类语法大致相似, 只是它依然是枚举类的匿名内部子类
9.5 包含抽象方法的枚举类
- 枚举类里定义抽象方法时不能使用 abstract 关键字将枚举类定义成抽象类(因为系统自动会添加 abstract 关键字),但因为枚举类需要显式创建枚举值, 而不是作为父类, 所以定义每个枚举值时必须为抽象方法提供实现, 否则将出现编译错误
示例代码
// 例子:
// Operation.java
public enum Operation{
PLUS{
public double eval(double x, double y){
return x + y;
}
},
MINUS{
public double eval(double x, double y){
return x - y;
}
},
TIMES{
public double eval(double x, double y){
return x * y;
}
},
DIVIDE{
public double eval(double x, double y){
return x / y;
}
};
// 为枚举类定义一个抽象方法(只要包含抽象方法的枚举类就是抽象枚举类, 抽象枚举类不用显式的使用 abstract 关键字)
// 这个抽象方法由不同的枚举值提供不同的实现
public abstract double eval(double x, double y);
public static void main(String[] args){
System.out.println(Operation.PLUS.eval(3, 4));
System.out.println(Operation.MINUS.eval(5, 4));
System.out.println(Operation.TIMES.eval(5, 4));
System.out.println(Operation.DIVIDE.eval(5, 4));
}
}
10 对象与垃圾回收
垃圾回收机制的特征
- 垃圾回收机制只负责回收堆内存中的对象, 不会回收任何物理资源(例如:数据库连接, 网络IO等资源)
- 程序无法精确控制垃圾回收的运行, 垃圾回收会在合适的时候运行, 当对象永久性地失去引用后, 系统就会在合适的时候回收它所占的内存
- 在垃圾回收机制回收任何变量之前, 总会先调用它的 finalize()方法, 该方法可能使该对象重新复活(让一个引用变量重新引用该变量), 从而导致垃圾回收机制取消回收
- 一个对象可以被一个方法的局部变量引用, 也可以被其他类的类变量引用, 或被其他对象的实例变量引用; 当某个对象被其他类的类变量引用时, 只有该类被销毁后, 该对象才会进入可恢复状态; 当某个对象被其他对象的实例变量引用时, 只有当该对象被销毁后, 该对象才会进入可恢复状态
10.1 对象在内存中的状态
状态分类
- (当一个对象在堆内存中运行时, 根据它被引用变量所引用的状态分类:)
- 可达状态: 当一个对象被创建后, 若有一个以上的引用变量引用它, 则这个对象在程序中处于可达状态, 程序可通过引用变量来调用该对象的实例变量和方法
- 可恢复状态: 如果程序中某个对象不再有任何引用变量引用它, 它就进入可恢复状态. 在这种状态下, 系统的垃圾回收机制准备回收该对象占用的内存, 在回收该对象之前, 系统会调用所有可恢复状态对象的 finalize() 方法进行资源清理.如果系统在调用该对象的 finalize() 方法时, 重新让变量引用了该对象, 则该对象再次进入可达状态, 否则该对象将进入不可达状态
- 不可达状态: 当对象与所有引用变量的关联都被切断, 且系统已经调用所有对象的 finalize() 方法后依然没有使该对象变成可达状态时, 系统才是真正回收该对象所占用的资源
10.2 强制垃圾回收
强制系统垃圾回收的两种方式
- 调用 System 类的 gc() 静态方法: System.gc()
- 调用 Runtime 对象的 gc() 实例方法: Runtime.getRuntime().gc()
示例代码
// 例子:
public class GcTest{
public static void main(String[] args){
for (int i = 0; i < 4; i++){
new GcTest();
// 下面两行代码的作用完全相同, 强制系统进行垃圾回收
// System.gc();
Runtime.getRuntime().gc();
}
}
public void finalize(){
System.out.println("系统正在清理GcTest对象资源...");
}
}
// 程序说明:
// 1. 编译后, 使用 java -verbose:gc GcTest 命令运行程序
// 2. -verbose:gc 这条命令可以看到每次垃圾回收后的提示信息
// 3. 垃圾回收机制会在收到通知后, 尽快进行垃圾回收
10.3 finalize 方法
- 在垃圾回收机制回收某个对象所占用的内存之前, 通常要求程序调用适当的方法来清理资源, 在没有明确指定清理资源的情况下, java提供了默认机制来清理该对象的资源, 这个机制就是 finalize() 方法
finalize() 方法的特点
- 永远不要主动调用某个对象的 finalize()方法, 该方法应交给垃圾回收机制调用
- finalize() 方法何时被调用, 是否被调用具有不确定性, 不要把 finalize()方法当成一定会被执行的方法
- 当JVM执行可恢复对象的finalize()方法时, 可能使该对象或系统中其他对象重新变成可达状态
- 当JVM执行 finalize()方法时出现异常时, 垃圾回收机制不会报告异常, 程序继续执行
10.4 对象的软, 弱和虚引用
- 软引用 弱引用和虚引用都包含了一个 get() 方法, 用于获取被他们所引用的对象
- 软引用 弱引用可以单独使用, 虚引用必须好引用队列(ReferenceQueue)联合使用才有意义
引用的分类
- 强引用 (StrongReference)
说明: 这是Java程序最常见的引用方式, 程序创建一个对象, 并把这个对象赋值给一个引用变量, 程序通过该引用变量来操作实际对象, 前面学的对象和数组否都采用了这种枪饮用的方式 - 软引用 (SoftReference)
说明: 软引用需要通过 SoftReference 类来实现, 当一个对象只有软引用时, 他有可能被垃圾回收机制回收. 对于只有软饮用的对象而言, 当系统内存空间足够时, 它不会被系统回收, 程序也可使用该对象, 当系统内存空间不足时, 系统可能会回收它. 软饮用通常用于对内存敏感的程序中 - 弱引用 (WeakReference)
说明: 弱引用通过 WeakReference 类来实现, 弱引用和软引用很像, 但弱引用的引用级别更低. 对于只有弱引用的对象而言, 当系统垃圾回收机制运行时, 不管系统内存是否足够, 总会回收该对象所占用的内存. - 虚引用 (phantomReference)
说明: 虚引用通过 phantomReference 类来实现, 虚引用完全类似于没有引用, 虚引用对对象本身没有太大影响, 对象甚至但觉不到虚引用的存在
11 修饰符的使用范围
- 记住P233页的表
- strictfp 关键字: 含义: FP-strict 也就是精确浮点的意思. strictfp修饰的类,接口或方法可以让浮点运算更加精确
- native 关键字: 主要用于修饰一个方法, 使用native修饰的方法类似于一个抽象方法
12. 使用 JAR 文件
- JAR文件的全称是 Java Archive File, 意思就是 Java 档案文件. 通常JAR文件是一种压缩文件, 与常见的ZIP压缩文件兼容通常也被称为JAR包,(与ZIP文件区别在于:JAR文件中默认包含了一个名为META-INF/MANIFRST.MF的清单文件)
- JAR文件通常使用jar命令压缩而成, 当使用jar命令压缩生成JAR文件时, 可以把一个或多个路径全部压缩成一个JAR文件
用途:
- 当把应用程序提供给别人使用时,通常会将这些类文件打包成一个JAR文件,把这个JAR文件提供给别人使用.只要别人在系统的CLASSPATH环境变量中添加这个JAR文件, 则JAVA虚拟机就可以自动在内存中解压这个JAR包, 把这个JAR文件当成一个路径,在这个路径中查找所需要的类或包层次对应的路径结构
JAR优点
- 安全; 对JAR文件进行数字签名, 只让识别数字签名的用户使用里面的东西
- 加快下载速度;
- 压缩; JAR压缩机制和ZIP完全相同
- 包封装; 能够让JAR包里面的文件以来与统一版本的类文件
- 可移植性;
通过例子来了解jar命令
-
创建JAR文件: jar cf test.jar test
说明:该命令不显示压缩过程, 执行结果是将当前路径下的 test 路径下的全部内容生成一个 test.jar 文件, 如果当前文件中包含 test.jar 文件, 那么该文件将被覆盖 -
创建JAR文件, 并显示压缩过程: jar cvf test.jar test
说明:该命令与上一条命名相同, 只是显示打包过程 -
不使用清单文件: jar cvfM test.jar test
说明:该命令与上一条命令类似, 其中的M选项表明不生成清单文件, 因此生成的test.jar中不包含META-INF/MANIFRST.MF的清单文件 -
自定义清单文件内容: jar cvfm test.jar manifest.mf test
- 运行结果和第二条命令类似, 其中m选项指定读取用户清单文件信息. 因此在生成的JAR包中清单文件META-INF/MANIFRST.MF的内容会有所不同, 他会在原有清单文件的基础上增加 MANIFRST.MF 文件内容
- 当开发者向 MANIFRST.MF 清单文件中增加自己的内容时, 就需要借助于自己的清单文件, 清单文件只是一个普通的文本文件, 清单文件的内容由如下格式的多个 key-value 对组成key:<空格>value
- 清单文件的格式要求:
- 每行只能定义一个 key-value 对, 每行的 key-value 对之前不能由空格, 即 key-value对必须顶格写
- 每组key-value 对之间以":"(英文冒号后紧跟一个空格)分割, 少写冒号或空格都是错误的
- 文件开头不能有空行
- 文件必须以一个空行结束
- 提取META-INF/MANIFRST.MF中的文件:
- 假设: 执行了 jar cvfm test.jar manifest.mf test 这条jar命令, 密钥文件默认为 a.txt
- 命令: jar cvfm test.jar a.txt test
-
查看JAR包内容: jar tf test.jar
说明:1. 在 test.jar 已经存在的前提下, 使用上面的命令可以查看test.jar包中的内容 -
查看JAR包详细内容: jar tvf test.jar
说明:1. 这条命令比1.5命令更加详细的显示 test.jar 的内容 -
加压缩: jar xf test.jar
- 假设: 使用 jar cvf test.jar test 这条命令来打包
- 这条命令会将上述打包的jar包解压缩, 不显示任何信息
-
带提示信息解压缩: jar xvf test.jar
说明:1. 类似于第7条命令, 只是会显示解压过程详细信息 -
更新JAR文件: jar uf test.jar Hello.class
说明:1. 更新test.jar 中的Hello.class文件, 如果test.jar中已有了Hello.class文件, 则使用新的Hello.class文件替换原来的Hello.class 文件, 如果 test.jar 中没有 Hello.class 文件, 则把新的 hello.class 文件添加到test.jar 文件中 -
更新时显示详细信息: jar uvf test.jar Hello.class
- 类似于第9条命令, 但是会显示详细的信息
- 创建可执行的 JAR 包
-
应用程序发布的三种方式:
- 使用平台相关的编译器将整个应用编译成平台相关的可执行文件
说明:1. 需要第三方编译器支持, 丧失了跨平台性, 可能带来性能下降 - 为应用编辑一个批处理文件
说明:
1. 如 windows 下的: java package.MainClass
2. 该命令运行时, 系统将执行批处理文件的java命令, 从而运行程序主类
3. start javaw package.MainClass 该命令运行不保留运行窗口 - 将一个应用程序制作成可执行的JAR包, 通过JAR包来发布应用程序
- 使用平台相关的编译器将整个应用编译成平台相关的可执行文件
-
将应用程序打包成JAR包:
命令:
jar cvfe test.jar test.Test test说明:
1. e 选项指定JAR包中作为程序入口的主类的类名
2. 创建可执行的JAR包的关键在于: 让javaw命令知道JAR包中那个类是主类运行JAR包的两种方式:
1. 使用 Java 命令, 使用Java运行时的语法是: java -jar test.jar
2. 使用 javaw 命令, 使用javaw运行时的语法是: javaw test.jar -
注意问题:
- 如果需要将文件解压缩到指定目录下, 则需要先将JAR文件拷贝到目标目录下, 在进行解压缩, 但是使用 unzip 命令时使用 -d 选项即可: unzip test.jar -d dest/
-