五、面向对象(下)
Java 为 8 个基本类型提供了对应的包装类,通过这些包装类可以将 8 个基本类型的值包装成对象来使用,JDK 1.5 提供了自动装箱和自动拆箱功能 ,允许把基本类型值赋给对应的包装类引用变量,也允许把包装类对象直接赋给对应的基本类型变量。
Java 提供了final
关键字来修饰变量、方法和类,系统不允许为final
变量重新赋值,子类不允许覆盖父类的 final 方法,final 类不能派生子类。通过使用final
关键字,允许 Java 实现不可变类,不可变类让系统变得更安全。
abstract
和interface
两个关键字用于定义抽象类和接口,抽象类和接口都是从多个子类中抽象出来的共同特征,但抽象类作为多个类的模板,而接口则定义了多类应该遵守的规范。Lambda 表达式是 Java 8 的重要更新,enum
关键字用于创建枚举类,枚举类是一种不能自由创建对象的类,枚举类的对象在定义类时已经固定下来。
5.1 包装类
Java 是面向对象的程序设计语言,但 8 种基本数据类型不支持面向对象的编程机制,也不具备“对象”的特性:没有成员变量、方法可以调用。
所有的引用类型变量都继承了 Object 类,都可以当成 Object 类型变量来使用,但基本数据类型的变量就不可以,如果某个方法需要 Object 类型的参数且值为常数,此时难以处理。
为解决 8 种基本数据类型的变量不能当成 Object 类型变量使用的问题,Java 提供了包装类(Wrapper Class)的概念,为 8 中基本数据类型定义了相应的引用类型,并称之为基本数据类型的包装类。
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
在JDK 1.5 以前,基本数据类型变量与包装类对象转换需要对应包装类的构造器或提供的实例方法实现,如图:
JDK 1.5 以后提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能。所谓自动装箱,就是可以把一个基本类型变量直接赋给对应的包装类变量,或者赋给 Object 变量(Object 类时所有类的父类,子类对象可以直接赋给父类对象);自动拆箱允许将包装类对象直接赋给一个对应的基本类型变量(值为 null 的引用变量自动拆箱赋值给一个基本类型变量抛出 java.lang.NullPointerException
空指针异常)。
自动装箱和自动拆箱时必须注意类型匹配,例如 Integer 只能自动拆箱成 int 类型变量,不要试图拆箱成 boolean 类型变量;与之类似的是,int 类型变量只能自动装箱成 Integer 对象(即使赋值给 Object 类型对象,也只是利用了 Java 的向上转型特性),不能装箱成 Boolean 对象。
包装类可以实现基本类型变量和字符串之间的转换,把字符串的值转换为基本类型的值有两种方式:
- 利用包装类提供的
parseXxx(String s)
静态方法(除了 Character 之外的包装类都提供了该方法); - 利用包装类提供的
Xxx(String s)
构造器;
此外 String 类提供了多个重载valueOf()
方法,用于将基本类型变量转换成字符串;也可以将基本类型变量和""
进行连接运算转换成字符串。
public class WrapperTest {
public static void main(String[] args) {
String intStr = "123";
//将特定的字符串转换成int变量,如果不是数字字符串抛出java.lang.NumberFormatException
int ival1 = Integer.parseInt(intStr);
int ival2 = new Integer(intStr);
System.out.println(ival2); //输出123
String floatStr = "123.456";
float fval1 = Float.parseFloat(floatStr);
float fval2 = new Float(floatStr);
System.out.println(fval1); //输出123.456
String str = String.valueOf(ival1);
System.out.println(str);
//float变量转换成String类型
str = String.valueOf(666.666f);
System.out.println(str);
}
}
包装类型的变量是引用数据类型,但可以进行比较:
- 包装类的实例可以与数值类型的值进行比较,这种比较是直接取出包装类实例所包装的数值类进行比较;
Integer ival = 3;
//输出true
System.out.println("3的包装类实例是否大于2 " + (ival > 2));
- 两个包装类型的实例进行比较,只有两个包装类引用到同一个对象时才会返回
true
;但自动装箱后的两个包装类进行比较会出现一些特殊情形;
//new的两个包装类实例
System.out.println("比较两个包装类的实例是否相等:" + (new Integer(2) == new Integer(2)));//输出false
//通过自动装箱的两个包装类实例
Integer ina = 2;
Integer inb = 2;
System.out.println("两个2自动装箱后是否相等:" + (ina == inb));//输出true
Integer biga = 128;
Integer bigb = 128;
System.out.println("两个128自动装箱后是否相等:" + (biga == bigb));//输出false
以上程序让人费解:2 和 128 自动装箱得到的结果并不相同,这与 Java 的 Integer 类的设计有关,查看 java.lang.Integer 类的源代码:
//定义一个长度为256的Integer数组
static final Integer[] cache = new Integer[-(-128) + 127 + 1];
static{
//执行初始化,创建-128到127的Integer实例,并放入cache数组中
for(int i = 0; i < cache.length; i++)
cache[i] = new Integer(i - 128);
}
可知系统把 -128 ~ 127 之间的整数自动装箱成 Integer 实例,并放入 cache 的数组中缓存起来,所以将-128 ~ 127 之间的同一个整数自动装箱成 Integer 实例时,永远都是引用 cache 数组的同一个数组元素,全部相等;而不在此范围的整数自动装箱时,系统总是重新 new 一个 Integer 实例,引用并不相等。
Java 7 还增强了包装类的功能,提供了静态的compare(xxx val1, xxx val2)
来比较两个基本类型值的大小,包括比较两个 boolean 类型值;Character 包装类中增加了大量的工具方法来对一个字符进行判断。包装类的equals
方法也重写,取出值 value 进行比较。
Java 8 中支持了无符号算术运算,为 Integer 和 Long 包装类提供了一下方法:
方法 | 功能 |
---|---|
static String toUnsignedString(int/long i) | 将指定 int 或 long 型整数转换为无符号整数对应的字符串 |
static String toUnsignedString(int/long i, int radix) | 将指定 int 或 long 型整数转换成指定进制无符号整数对应的字符串 |
static xxx parseUnsignedXxx(String s) | 将指定字符串解析成无符号整数 |
static xxx parseUnsignedXxx(String s, int radix) | 将指定字符串按照指定进制解析成无符号整数 |
static int compareUnsigned(xxx x, xxx y) | 将 x,y 两个整数转换成无符号整数后比较大小 |
static long divideUnsigned(long dividend, long divisor) | 将 x,y 两个整数转换成无符号整数后计算他们相除的商 |
static long remainderUnsigned(long dividend, long divisor) | 将 x,y 两个整数转换成无符号整数后计算他们相除的余数 |
Java 8 还为 Byte、Short 增加了toUnsignedInt(xxx x)
、toUnsignedLong(yyy x)
两个方法,将指定 byte 和 short 类型的变量或值转换成无符号的 int 或 long 型数值。
5.2 处理对象
Java 对象都是 Object 类的实例,都可以直接调用该类中的方法。
5.2.1 打印对象
一个对象直接打印到控制台输出字符串时,实际输出的是该对象的toString()
方法的返回值,toString()
方法是 Object 类里的一个实例方法,所有的类都是 Object 类的子类,因此所有的 Java 对象都具有该方法。不仅如此,所有的 Java 对象与字符串连接运算时,系统自动调用 Java 对象的toString()
方法的返回值和字符串进行连接运算。
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
public class Print {
public static void main(String[] args) {
Person p = new Person("wang");
//输出com.test.Person@28d93b30,包含了包名
System.out.println(p);
System.out.println("" + p);
}
}
toString()
方法是一个“自我描述”的方法,Object 类提供的该方法总是返回该对象实现类的“类名+@+hashCode”值,用户可自定义重写该方法 。
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + '}';
}
5.2.2 ==和equals方法
Java 中测试两个变量是否相等有两种方式:==
和equals()
方法:
==
判断时,如果两个变量是基本类型变量,且都是数值类型(并不一定要求数据类型严格相同),则只需要两个变量值相等返回true
;如果是两个引用类型变量,只有它们都指向同一个对象时,才会返回true
;==
不可用于比较类型上没有父子关系的两个对象。
public class Test {
public static void main(String args[]) {
int ival = 65;
float fval = 65.0f;
System.out.println(ival == fval);//输出true
char ch = 'A';
System.out.println(65 == ch);//输出true
String str1 = "he" + "llo";
String str2 = "hello";
System.out.println(str1 == str2);//输出true
String str3 = new String("Java");
String str4 = new String("Java");
System.out.println(str3 == str4);//输出false
System.out.println(str3.equals(str4));//输出true
//由于java.lang.String与Test没有继承关系,编译出错
//System.out.println(str4 == new Test());
}
}
"hello"
直接量和 new String("hello")
的区别:Java 程序直接使用字符串直接量(包括可以在编译时就计算出来的字符串值),JVM 将会使用常量池来管理这些字符串;当使用new String("hello")
时,JVM 会先使用常量池来管理"hello"
直接量,再调用 String 类的构造器来创建一个新的字符串对象保存在堆内存中。
常量池(constant pool)专门用于管理在编译期间被确定并被保存在已编译的 .class 文件中的一些数据,包括类、方法、接口和字符串常量。Java 常量池保证相同的字符串直接量只有一个,不会产生多余的副本。JDK 7 将常量池的位置从 JVM 的方法区中改到存放堆中。
equals()
方法是 Object 类提供的一个实例方法,因此所有引用变量都可调用该方法来判断是否与其他引用变量相等,但这个方法判断两个对象相等与使用==
没有区别,只是比较对象的地址,同样要求两个引用变量指向同一个对象才会返回true
,可以重写该方法实现比较。
String 类已经重写了 Object 的 equals() 方法,String 的 equals() 方法判断两个字符串相等的标准是:只要两个字符串所包含的字符序列相同,通过 equals 比较将返回 true。
class Person {
private String name;
private String idstr;
public Person() {
}
public Person(String name, String idstr) {
this.name = name;
this.idstr = idstr;
}
//getter和setter
public String getName() {
return name;
}
public String getIdstr() {
return idstr;
}
public void setName(String name) {
this.name = name;
}
public void setIdstr(String idstr) {
this.idstr = idstr;
}
//重写equals()方法,提供自定义的相等比较方法
@Override
public boolean equals(Object obj) {
//如果两个对象时同一个对象
if (this == obj)
return true;
//obj是Person对象
if (obj != null && obj.getClass() == Person.class) {
Person personObj = (Person) obj;
//当前对象的idstr与obj对象的idstr相等才可以判断两个对象相等
if (this.getIdstr().equals(this.getIdstr()))
return true;
}
return false;
}
}
public class Test {
public static void main(String args[]) {
Person p1 = new Person("wang", "123456");
Person p2 = new Person("zhang", "123456");
System.out.println(p1.equals(p2));//输出true
}
}
判断 obj 是否是 Person 类的实例时,instanceof 运算符是判断前面对象是否是后面类、子类、实现类的实例,不符合该种情况,因此要求两个对象时同一个类的实例时,使用 obj.getClass == Person.class 语句。特殊的,String 类是 final 修饰的不可变类,不可能有子类和实现类,equals 方法用 instanceof 运算符判断,同理,final 修饰的不可变类都可以用 instanceof 来判断,且不用多余判断 null 情况。
重写 equals 方法应该满足以下条件:
- 自反性:对任意的 x,
x.equals(x)
返回true
; - 对称性:对任意的 x 和 y,
x.equals(y)
和y.equals(x)
返回结果相同; - 传递性:对任意的 x,y,z,如果
x.equals(y)
和y.equals(z)
返回true
,则x.equals(z)
返回true
; - 一致性:对任意的 x,y,无论什么时候调用返回结果应该一致;
- 对任何不是
null
的 x,x.equals(null)
返回false
;
String 类重写 equals 方法的源码:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
5.3 类成员
static
关键字修饰的成员就是类成员,类成员属于整个类,而不属于单个对象,类成员不能访问实例成员。
5.3.1 理解类成员
Java 类中包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举),其中 static 可修饰的成员变量、方法、初始化块、内部类(包括接口、枚举)。类变量属于整个类,当系统第一次准备使用该类时,会为该类变量分配内存空间,类开始生效直到类被卸载,该类的类变量所占有的内存才被系统的垃圾回收机制回收。同一个类的所有对象访问类变量时,实际上访问的都是该类持有的变量。注意:static 关键字修饰不能在方法中使用。
**当使用实例来访问类成员时,实际上是委托给该类来访问类成员,因此即使某个实例的值为 null,它也能访问它所属类的类成员。**但如果一个 null 对象访问实例成员(包括实例变量和实例成员),将会引发 NullPointerException 异常。
5.3.2 单例类
如果一个类始终只能创建一个实例,则这个类被称为单例类(Singleton)。在一些特殊的场景下,要求不允许自由创建该类的对象,而只允许为该类创建一个对象。为了避免其他类自由创建该类的实例,应该把该类的构造器用private
修饰,从而把该类的所有构造器隐藏起来。
良好封装原则:一旦把该类的构造器隐藏起来,就需要提供一个 public 方法作为该类的访问点,用于创建该类的对象,且该方法必须使用 static 修饰(因为调用该方法之前不存在对象,所以调用者必然是类)
除此之外,该类必须缓存已经创建的对象,否则无法知道曾经创建过对象,也就无法保证只创建一个对象。所以需要一个成员变量来保存曾经创建过的对象,由于需要被静态方法访问,成员变量也必须用static
修饰。
/**
* 懒汉式
* 这种方式是最基本的单例模式实现方式,这种实现最大的问题就是不支持多线程。
* 因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
* 这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作
*/
public class Singleton {
//使用一个类变量来缓存曾经创建的实例
private static Singleton instance;
//对构造器使用private修饰,隐藏构造器
private Singleton() {
}
//提供一个静态方法,用于返回Singleton实例
public static Singleton getInstance() {
//如果instance为null,则表明不曾创建Singleton对象
//如果instance不为null,则表明已经创建Singleton对象,不会再创建
if (instance == null) {
//创建一个单例类实例并保存起来
instance = new Singleton();
}
return instance;
}
}
5.4 final修饰符
final 关键字可用于修饰类、变量和方法,表示修饰的类、变量和方法不可变。final
修饰变量时,表示一旦该变量获得了初始值就不可被改变,final
既可以修饰成员变量,也可以修饰局部变量。
5.4.1 final成员变量
成员变量是随类或对象初始化来初始化的,当类初始化时,系统会为该类的类变量分配默认值;当创建对象时,系统会为该对象的实例变量分配内存并分配默认值。对于final
修饰的成员变量,一旦有了初始值就不能重新赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造器中指定初始值,那么这些成员变量值一直是系统分配的 0、’\u0000’、false、null,失去存在意义。因此Java 语法规定:final 修饰的成员变量必须由程序员显式地指定初始值,系统不会对 final 成员隐式初始化。
- 类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能两个地方之一指定;
- 实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能三个地方之一指定;
- 普通方法中不能为
final
修饰的成员变量赋值,如果打算在构造器、初始化块中对final
成员变量初始化,则不要在初始化之前就访问成员变量的值;
final 修饰的实例变量不能在静态初始化块中指定初始值,因为静态初始化块是静态成员,不可访问非静态成员实例变量;final 修饰的类变量不能在普通初始化块中指定初始值,因为类变量已经在类初始化阶段初始化,普通初始化块不能对其重新赋值。
public class FinalTest {
//定义成员变量时指定默认值,合法
final int a = 6;
//以下变量将在构造器或初始化块中分配初始值
final String str;
final int c;
final static double d;
//ch没有显式指定初始值,不合法
//final char ch;
{
//初始化块中为实例变量指定初始值,合法
str = "Hello";
//定义时已经指定初始值,不合法
//a = 9;
}
static {
//静态初始化块中类变量指定初始值
d = 5.6;
}
//构造器中指定初始值
public FinalTest() {
this.c = 5;
}
public void changeFinal() {
//普通方法不能为final修饰的成员变量赋值或指定初始值
//d = 1.2;
//ch = 'a';
}
public static void main(String[] args) {
FinalTest ft = new FinalTest();
System.out.println(ft.a);
System.out.println(ft.c);
System.out.println(FinalTest.d);
}
}
5.4.2 final局部变量
系统不会对局部变量初始化,局部变量必须由程序员显式初始化,因此使用final
修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值,在后面代码中对该final
变量赋初始值且不能重复赋值。特别地,final
修饰的形参会在调用方法时根据实参初始化,因此使用 final 修饰的形参不能被赋值。
5.4.3 final修饰变量
final 修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量的值不能改变;final 修饰引用类型变量时,final 只保证引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
import java.util.Arrays;
class Person {
private String name;
private String idstr;
public static int num = 666;
public Person() {
}
public Person(String name, String idstr) {
this.name = name;
this.idstr = idstr;
}
//getter和setter
public String getName() {
return name;
}
public String getIdstr() {
return idstr;
}
public void setName(String name) {
this.name = name;
}
public void setIdstr(String idstr) {
this.idstr = idstr;
}
}
public class Test {
public static void main(String args[]) {
final int[] arr = new int[]{5, 6, 12, 9};
System.out.println(Arrays.toString(arr));
//对数组进行排序,合法
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
//对数组进行赋值,合法
arr[2] = 7;
System.out.println(Arrays.toString(arr));
//对arr重新赋值,不合法
//arr = null;
final Person p = new Person("wang", "java");
//改变Person对象的实例变量,合法
p.setName("zhang");
System.out.println(p.getName());
//对p重新赋值,不合法
//p = null;
}
}
对于final
修饰的变量,无论是类变量、实例变量还是局部变量,只要变量满足以下条件,final
变量相当于一个直接量:
final
修饰符修饰;- 在定义时指定初始值
- 初始值可以在编译时被确定,即被赋表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量、调用方法;
final 修饰符一个重要用途就是定义宏变量,当定义 final 变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个 final 变量本质上就是一个宏变量,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
public class StringJoinTest {
public static void main(String[] args) {
String s1 = "HelloJava";
//s2直接引用常量池中已有的字符串
String s2 = "Hello" + "Java";
System.out.println(s1 == s2);//输出true
/*
由于str3是由str1和str2连接得到的,且str1和str2是普通变量
编译器无法进行宏替换
如果想让str3 == s1返回true,只要让编译器对str1和str2执行宏替换
也就是把str1和str2使用final修饰符
*/
String str1 = "Hello";
String str2 = "Java";
String str3 = str1 + str2;
System.out.println(s1 == str3);//输出false
}
}
5.4.4 final方法
final 修饰的方法不能被重写,即子类不能重写父类的某个方法。Java 提供的 Object 类就有一个 final 方法:getClass(),因为不希望任何类重写这个方法;但对于该类提供的 toString() 和 equal() 方法,都允许子类重写。final 修饰的方法仅仅是不能被重写,并不是不能被重载。
对于一个 private 方法,因为仅在当前类可见,其子类无法访问该方法,所以子类无法重写该方法。如果子类中定义一个与父类 private 方法有相同方法名、形参列表和返回值也不是重写,所以可以用 final 修饰一个 private 方法,子类仍然可以重写。
5.4.5 final类
final 修饰的类不能有子类,例如 java.lang.Math 就是一个 final
类,不可以有子类。
5.5 抽象类
5.5.1 抽象方法和抽象类
抽象方法和抽象类必须使用 abstract 修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类可以没有抽象方法。
- 抽象类和抽象方法必须用
abstract
修饰,抽象方法不能有方法体; - 抽象类不能被实例化,无法使用
new
关键字来调用抽象类的构造器创建抽象类的实例,即使抽象类里不包含抽象方法也不能创建实例; - 抽象类可以含有成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接口、枚举)。抽象类的构造器不能用于创建实例,主要用于被子类调用。
- 含有抽象方法的类(定义了抽象方法;继承了抽象父类,但没有完全实现父类包含的抽象方法;实现了接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义为抽象类。
定义抽象方法只需要在普通方法上增加 abstract 修饰符,并把普通方法的方法体(也就是花括号括起来的部分)全部去掉,并在方法后增加分号即可。
定义抽象类只需在普通类上增加 abstract 修饰符即可。
总结起来就是,抽象类可以包含抽象方法,但不能创建实例。
public abstract class Shape {
{
System.out.println("执行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;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
Shape 类包含了两个抽象方法calPerimeter()
和getType()
,所以 Shape 只能定义为抽象类。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.a = a;
this.b = b;
this.c = c;
}
public void setSides(double a, double b, double c) {
if (a >= b + c || b >= a + c || c >= a + b) {
System.out.println("三角形两边之和大于第三边");
}
this.a = a;
this.b = b;
this.c = c;
}
//重写Shape类的计算周长的抽象方法
@Override
public double calPerimeter() {
return a + b + c;
}
//重写Shape类的返回形状的抽象方法
@Override
public String getType() {
return "三角形";
}
}
Triangle 类继承了 Shape 抽象类,并实现了 Shape 类的两个抽象方法,是一个普通类,因此可以创建 Triangle 类的实例,将一个 Shape 类型的引用变量指向 Triangle 对象。
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类的计算周长的抽象方法
@Override
public double calPerimeter() {
return 2 * Math.PI * radius;
}
//重写Shape类的返回形状的抽象方法
@Override
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());
}
}
abstract、final、static(修饰类成员) 的联系与区别:
修饰符 | 类 | 成员变量 | 局部变量 | 方法 | 构造器 | 初始化块 | 内部类 |
---|---|---|---|---|---|---|---|
abstract | 抽象类(不能实例化,只能被继承) | - | - | 抽象方法(子类重写) | - | - | - |
final | 不可被继承,与抽象类冲突 | 显示指定初始值 | 不能重复赋值 | 不可被重写 | - | - | - |
static | - | 静态变量 | - | 静态方法 | - | 静态初始化块 | 静态内部类 |
abstract
修饰的类只能被继承,final 修饰的类不能被继承。abstract
修饰的方法只能被子类重写,而final修饰的方法不能被重写,因此final
和abstract
永远不能同时使用;abstract
不能修饰变量和构造器;abstract
不能和static
同时修饰一个方法,即没有类抽象方法,因为abstract
修饰的方法是抽象方法,static
修饰的方法是类方法,要调用这个方法就是用抽象类调用抽象方法,由于抽象方法没有方法体,调用会出错(类方法只能继承不能被重写)。但是abstract
和static
可以同时修饰内部类;private
与abstract
不能同修饰一个方法。不然这个抽象方法无法被子类继承,无法被实现,也就没了意义;
5.5.2 抽象类的作用
从语义的角度来看,抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。抽象类体现的是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会大致保留抽象类的行为方式
5.6 接口
抽象类是从多个类中抽象出来的模板,如果将这种抽象进行的更彻底就是接口,接口中不能包含普通方法,接口内的所有方法都是抽象方法,Java 8 允许在接口中定义默认方法,默认方法可以提供方法实现。
5.6.1 接口的概念
接口是从多个相似类中抽象出来的规范,不提供任何实现,接口体现的是规范和实现分离的设计哲学。因此,接口定义的是多个类共同行为规范,这些行为是与外部交流的通道,这意味着接口通常是定义一组公共方法。
类是一种具体实现体,同一个类的内部状态数据、各种方法实现细节完全相同;而接口定义了某一批类所需遵守的规范,并不关心这些类的内部状态数据,也不关心这些类方法的实现细节,它只规定这些类必须提供某些方法满足实际需要。
定义接口的基本语法为:
[修饰符] interface 接口名 extends 父接口1, 父接口2...
{
常量定义
抽象方法定义
内部类、接口、枚举定义
默认方法或类方法定义
}
- 接口修饰符可以是
public
或默认不写(包权限)。一个接口可以有多个父接口,但接口只能继承接口,不能继承类; - 接口定义是一种规范,里面没有构造器、初始化块。接口的成员包括成员变量(只能是是常量)、方法(只能是抽象方法、类方法和默认方法)、内部类(包括内部接口、枚举);
- 接口的所有成员,包括常量、方法、内部类和内部枚举都是
public
访问权限。 - 接口中的成员变量系统会自动增加
static
和final
两个修饰符,也就是说接口里的成员变量都是用public static final
来修饰,而接口中没有构造器和初始化块,所以接口中定义的成员变量只能在定义时指定默认值。
接口里定义成员变量采用如下两行代码结果相同:
//系统自动为接口定义的成员变量增加 public static final 修饰符
int MAX_SIZE = 50;
public static final int MAX_SIZE = 50;
接口定义的方法只能是抽象方法、类方法或默认方法:
- 系统将自动为普通方法增加
abstract
修饰符,定义接口里的普通方法不管是否使用public abstract
修饰符,接口里的普通方法总是使用public abstract
来修饰。接口里的普通方法不能有方法实现(方法体),但类方法、默认方法必须有方法实现。 - Java 8 允许在接口中定义默认方法,默认方法必须使用
default
修饰,该方法不能使用static
修饰,所以需要实现类的实例来调用。无论程序是否指定,默认方法总是使用public
修饰。 - Java 8 允许在接口中定义类方法,类方法必须使用
static
来修饰,不能使用default
修饰,类方法总是使用public
修饰,可以直接使用接口调用。
public interface Output {
//接口内定义的成员变量只能是常量
int MAX_SIZE = 50;
//接口内定义的普通方法只能是public的抽象方法
void out();
void getData(String 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 "接口里的类方法";
}
}
接口里的成员变量默认使用 public static final 修饰;接口里的普通方法默认使用 public abstract 来修饰;接口里定义的内部类、内部接口、内部枚举都默认使用 public static 来修饰;显式指定不符合会编译错误。
从某种角度看,接口可以被当成一个特殊的类,因此一个 Java 源文件中最多只能有一个public 接口,且源文件的主文件名与该接口名相同。
5.6.2 接口的继承
接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以由多个直接父接口。和类继承相似,子接口扩展某个 父接口,将会获得父接口理定义的所有抽象方法、常量。一个接口继承多个父接口时,多个父接口排在extends
关键字之后。
interface interfaceA {
int PROP_A = 5;
void testA();
}
interface interfaceB {
int PORP_B = 6;
void testB();
}
interface interfaceC extends interfaceA, interfaceB {
int PROP_C = 10;
void testC();
}
public class InterfaceExtendsTest {
public static void main(String[] args) {
System.out.println(interfaceA.PROP_A);
System.out.println(interfaceB.PORP_B);
//可以通过interfaceC来访问PROP_A、PROP_B、PROP_C
System.out.println(interfaceC.PROP_C);
}
}
5.6.3 接口的使用
接口的主要用途就是被实现类实现,接口不能用于创建实例,但接口可以用于声明引用类型变量 。当使用接口来声明引用类型变量时,该变量必须引用到其实现类的对象。接口有以下作用:
- 定义变量,也可用于进行强制类型转换;
- 调用接口中定义的常量;
- 被其他类实现;
[修饰符] class 类名 extends 父类 implements 接口1,接口2...
{
类体部分
}
一个类可以实现一个或多个接口,继承使用extends
关键字,实现则使用implements
关键字,实现接口和继承父类类似,都可以获得所实现接口定义的常量(成员变量)、方法(包括抽象方法和默认方法)。一个类必须完全实现这些接口所定义的全部抽象方法(重写),否则该类将保留从父接口那里继承到的抽象方法,该类也必须定义为抽象类。
interface Product {
int getProductTime();
}
public class Printer implements Output, Product {
private String[] printData = new String[MAX_CACHE_LINE];
//记录当前需打印的作业数
private int dataNum = 0;
@Override
public void out() {
//只要还有作业就打印
while (dataNum > 0) {
System.out.println("打印机打印:" + printData[0]);
//把作业队列整体前移一位,并将剩下的作业数减一
//public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);
System.arraycopy(printData, 1, printData, 0, --dataNum);
}
}
@Override
public void getData(String msg) {
if (dataNum >= MAX_CACHE_LINE) {
System.out.println("输出队列已满,添加失败!");
} else {
//把打印数据添加到队列里,已保存数据的数量加1
printData[dataNum++] = msg;
}
}
@Override
public int getProductTime() {
return 45;
}
public static void main(String[] args) {
//创建一个Printer对象当做Output使用
Output o = new Printer();
o.getData("Hello");
o.getData("Java");
o.out();
o.getData("接口");
o.out();
//调用Output接口中定义的默认方法
o.print("优快云博客", "Java系列");
o.test();
//创建一个Printer对象当做Output使用
Product p = new Printer();
System.out.println(p.getProductTime());
}
}
5.6.4 接口和抽象类
接口和抽象类的联系:
- 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承;
- 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法
接口和抽象类设计上的差别:
- 接口作为系统和外界交互的窗口,体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务(如何来调用方法)。一个程序使用接口时,接口是多个模块耦合的标准;多个程序之间使用接口时,接口是多个程序之间的通信标准。接口类似系统的总纲,制定了系统各模块应该遵循的标准,不应该常发生改变。
- 抽象类作为系统中多个子类的共同父类,体现的是一种模板式设计。抽象类可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品不能当成最终产品,必须更进一步不同方式的完善。
接口和抽象类用法上的差别:
- 接口只能包含抽象方法、类方法和默认方法,不能为普通方法提供方法实现;抽象类则完全包含普通方法;
- 接口只能定义静态常量;抽象类则既可以定义普通成员变量,也可以定义静态常量;
- 接口里不包含构造器;抽象类里可以包含构造器,抽象类中的构造器并不是用于创建对象,而是让子类调用这些构造器来完成属于抽象类的初始化操作;
- 接口里不能包含初始化块;抽象类则完全可以包含初始化块;
- 一个类最多只有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口弥补 Java 单继承的不足。
5.7 内部类
内部类是定义在其他类的内部的类,包含内部类的类也称为外部类,内部类由以下作用:
- 内部类提供了更好的封装 ,把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类;
- 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量;
- 匿名内部类适合用于创建那些仅需要一次使用的类;
内部类与外部类的语法大致相同,存在以下两点区别:
- 内部类比外部类多使用三个修饰符:private、protected、static;
- 非静态内部类不能拥有静态成员;
5.7.1 非静态内部类
Java 不允许在非静态内部类中定义静态成员(静态成员变量、静态方法、静态初始化块),定义内部类只需要把一个类放在另一个类内部(包括方法中)即可,方法中定义的内部类被称为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员,局部内部类和匿名内部类则不是类成员。
成员内部类分为静态内部类和非静态内部类:
外部类上一级程序单元是包,所以有两个作用域:同一个包内和任何位置;而内部类上一级是程序单元是外部类,所以有4个作用域:同一个类、同一个包、父子类和任何位置。
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;
}
//省略getter和setter
//非静态内部类的实例方法
public void info() {
System.out.println("当前牛腿的颜色是:" + color + ", 高:" + length);
//直接访问外部类的private修饰的成员变量
System.out.println("本牛腿所在奶牛重:" + weight);
}
}
public void test() {
CowLeg cl = new CowLeg(1.12, "黑白相间");
cl.info();
}
public static void main(String[] args) {
Cow cow = new Cow(378.9);
cow.test();
}
}
编译程序,文件所在路径生成了两个 .class 文件,一个是 Cow.class
,另一个是 Cow$CowLeg.class
文件,前者是外部类 Cow 的 .class 文件,后者是内部类 CowLeg 的 .class 文件,即内部类的 class 文件总是这种形式:OuterClass$InnerClass.class
。非静态内部类可以访问外部类中的 private 成员,这是因为在非静态内部类对象里保存了一个它所寄生的外部类对象的引用(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,非静态内部类实例必须寄生在外部类实例中)。
非静态内部类对象和外部类对象关系:非静态内部类对象必须寄生在外部类对象里,而外部类对象则不必一定有非静态内部类对象寄生其中。简单的说,如果存在一个非静态内部类对象,则一定存在一个被它寄生的外部类对象。但外部类对象存在时,外部类对象里不一定寄生了非静态内部类对象。因此外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在,而非静态内部类对象访问访问外部类对象时,外部类对象一定存在。
非静态内部类中方法访问某个变量时(未显式指定调用者),系统查找的顺序如下:
- 该方法内查找是否存在该名字的局部变量;
- 该方法所在的内部类查找是否存在该名字的局部变量;
- 该内部类所在的外部类中查找是否存在该名字的成员变量(包括继承的成员变量),如果依然不存在,则编译错误,提示找不到该变量;
class var {
protected String name = "外部类父类实例变量";
}
public class DiscernVariable extends var {
private String prop = "外部类的实例变量";
private String name = "外部类继承的实例变量";
private class Inclass {
private String prop = "内部类的实例变量";
public void info() {
String prop = "局部变量";
//通过外部类名.this.varName访问外部类实例变量
System.out.println(DiscernVariable.this.prop);
System.out.println(DiscernVariable.this.name);
//通过this.varName访问内部类实例变量
System.out.println(this.prop);
//直接访问局部变量
System.out.println(prop);
//通过外部类名.super.varName访问外部类父类的实例变量
System.out.println(DiscernVariable.super.name);
}
}
public void test() {
Inclass in = new Inclass();
in.info();
}
public static void main(String[] args) {
new DiscernVariable().test();
}
}
如果外部类成员变量、内部类成员变量与内部类局部变量同名,通过OutterClass.this.varName
形式访问外部类的实例变量,通过this.propName
访问非静态内部类得实例变量。而非静态内部类的成员只在非静态内部类范围内可知,并不能被外部类直接使用。如果外部类需要访问非静态内部类的成员,则必须显式创建非静态内部类对象来调用其实例成员,否则非静态内部类对象不存在也就无法调用其实例方法。
public class Outer {
private int outProp = 9;
private void test() {
System.out.println("外部类的private实例方法");
}
private static void test(String msg) {
System.out.println("外部类的private类方法");
}
class Inner {
private int inProp = 5;
public void accessOuterProp() {
//非静态内部类可以直接访问外部类的private成员变量和方法
System.out.println(outProp);
test();
test("Java");
}
}
public void accessInnerProp() {
//外部类不能直接访问内部类的private变量,编译错误
//System.out.println(inProp);
//显式创建内部类的对象访问内部类的实例变量
System.out.println(new Inner().inProp);
}
public static void main(String[] args) {
Outer out = new Outer();
out.accessInnerProp();
}
}
注意到,private 有两种“失效”的情况:
- 内部类里面访问外部类的 private 成员变量或者方法;因为当内部类调用外部类的私有属性时,其真正的执行是调用了编译器生成的属性的静态方法(即 acess$0,access$1等)来获取这些属性值;
- 外部类显式创建内部类的对象访问内部类的 private 成员变量或方法,同样编译器又自动生成了一个获取私有属性的静态方法 access$0 一次来获取 inProp 的值;
- 可以理解为内部类和其他类成员都是外部类平等的类成员,只不过内部类包装了成员变量和方法,还是允许外部类创建内部类对象访问,但不能在外部类静态成员中使用非静态内部类定义变量和创建实例。
5.7.2 静态内部类
如果使用static
修饰一个内部类,那这个内部类属于外部类本身,而不属于外部类的某个对象,被称为类内部类或静态内部类。静态内部类可以包含静态成员和非静态成员,根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。
public class StaticInnerClassTest {
private int val1 = 5;
private static int val2 = 10;
static class StaticInnerClass {
//静态内部类可以包含静态和非静态成员
private int num = 66;
private static String name = "Java";
public void accessOuterVal() {
//静态内部类无法访问外部类的实例变量
//System.out.println(val1);
System.out.println(val2);
}
}
}
静态内部类的实例方法不能访问外部类的实例属性:因为静态内部类是外部类相关的,而不是外部类对象相关的。静态内部类对象不是寄生在外部类的实例中,而是寄生在外部类的类本身。当静态内部类对象存在时,并不存在一个被它寄生的外部类对象,静态内部类对象只持有外部类的类引用,没有持有外部类对象的引用。
静态内部类是外部类的一个静态成员,因此外部类的所有方法、所有初始化块中可以使用静态内部类来定义变量、创建对象等。而外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。
public class AccessStaticInnerClass {
static class StaticInnerClass {
private static int staticInnerProp = 2;
private int innerProp = 3;
}
public void accessInnerProp() {
//通过类名访问静态内部类的类成员
System.out.println(StaticInnerClass.staticInnerProp);
//通过实例访问静态内部类的实例成员
System.out.println(new StaticInnerClass().innerProp);
}
}
除此之外,Java 还允许在接口内定义内部类,接口里定义的内部类默认使用 public static
修饰,即接口内部类只能是 public 访问权限的静态内部类。
5.7.3 使用内部类
- 外部类内部使用内部类:
不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类; - 外部类以外使用非静态内部类: 内部类不能使用 private 修饰,对于其他访问控制修饰符,则能在对应的访问权限内使用。在外部类以外的地方定义内部类需要外部类完整的类名(包括包名),由于非静态内部类的对象必须寄生在外部类对象里,因此创建非静态内部类对象之前,必须先创建外部类对象,然后 new 调用非静态内部类的构造器,即非静态内部类的构造器必须使用其外部类对象来调用:
//外部类以外定义静态和非静态内部类
//OuterClass.InnerClass varName
//外部类以外创建非静态内部类对象
//OuterInstance.new InnerConstructor()
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("测试");
/*
上面代码可改为
定义内部类变量
Out.In in;
创建外部类实例,非静态内部类实例寄生在该实例中
Out out = new Out();
调用外部类实例和new来调用内部类构造器创建非静态内部类实例
in = out.new In("测试");
*/
}
}
特别地,当创建一个子类时,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,即必须存在一个外部对象。
public class SubClass extends Out.In
{
public SubClass(Out out)
{
//通过传入的Out对象显式调用In的构造器
out.super("hello");
}
}
非静态内部类 In 类的构造器必须使用外部类对象来调用,代码中 super 代表调用 In 类的构造器,而 out 则代表外部类对象。非静态内部类 In 对象和 SubClass 对象都必须持有指向 Outer 对象的引用,区别是创建两个对象时传入 Out 对象的方式不同:当创建非静态内部类 In 类的对象时,必须通过 Outer 对象来调用 new 关键字;当创建 SubClass 类的对象时,必须使用 Outer 对象作为调用者来调用 In 类的构造器。
非静态内部类的子类不一定是内部类,它可以是一个外部类。但非静态内部类的子类实例一样需要保留一个引用,该引用指向其父类(内部类)所在外部类的对象。也就是说,一个内部类子类的对象一定存在与之对应的外部类对象。
- 外部类以外使用静态内部类:
静态内部类是外部类类相关的,因此创建静态内部类对象时无需创建外部类对象。
//创建静态内部类对象
//OuterClass.InnerClass in = new OuterClass.InnerConstructor()
class StaticOut {
//定义一个静态内部类,不使用访问控制符的包访问控制权限
static class StaticIn {
public StaticIn() {
System.out.println("静态内部类的构造器");
}
}
}
//定义静态内部类的子类
class StaticSubClass extends StaticOut.StaticIn {}
public class CreateStaticInstance {
public static void main(String[] args) {
StaticOut.StaticIn in = new StaticOut.StaticIn();
}
}
可以看出,静态内部类和非静态内部类声明变量的语法一样,区别只是在创建内部类对象时,静态内部列只需要使用外部类即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器。
内部类是外部类的成员,但不能为外部类定义子类,在子类中在定义一个内部类来重写其父类中的子类。因为内部列的类名不再是简单的内部类的类名组成,实际上还把外部类的类名作为一个命名空间作为内部类类名的限制。因此子类中的内部类和父类中的内部类不可能完全同名,也就不可能重写。
5.7.4 局部内部类
局部内部类很少使用,如果把一个内部类放在方法里定义,这个内部类就是一个局部内部类。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和 staic 修饰符修饰。局部内部类的 class 文件总是遵循如下命名格式:OuterClass$NInnerClass.class
,N是一个数字用于区分同名的局部内部类(处于不同方法中)。
5.7.5 匿名内部类
匿名内部类适合创建那种只需要一次使用的类,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,不能重复使用。匿名内部类必须继承最多一个父类或实现一个接口,有以下规则:
- 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象,因此不允许将匿名内部类定义成抽象类;
- 匿名内部类不能定义构造器,因为没有类名;
- 匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情;
interface Products {
public double getPrice();
public String getName();
}
class AnonymousProduct implements Products {
@Override
public double getPrice() {
return 567.8;
}
@Override
public String getName() {
return "显卡";
}
}
public class AnonymousTest {
public void test(Products p) {
System.out.println("购买一个" + p.getName() + ", 花掉" + p.getPrice());
}
public static void main(String[] args) {
AnonymousTest at = new AnonymousTest();
at.test(new Products() {
@Override
public double getPrice() {
return 567.8;
}
@Override
public String getName() {
return "显卡";
}
});
//创建Product实现类来达到匿名类相同效果,代码复杂
at.test(new AnonymousProduct());
}
}
定义匿名内部类无需 class 关键字,而是在定义匿名内部类时直接生成该匿名内部类的对象。由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父类或接口里包含的所有抽象方法。通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故 new 接口名后的括号不能传入参数值。但如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器(相同的形参列表)。
abstract class Device {
private String name;
public abstract double getPrice();
public Device() {}
public Device(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class AnonymousInner {
public void test(Device de)
{
System.out.println("购买一个" + de.getName() + ", 花掉" + de.getPrice());
}
public static void main(String[] args) {
AnonymousInner ai = new AnonymousInner();
//调用有参数的构造器创建Device匿名实现类的对象
ai.test(new Device("显卡") {
@Override
public double getPrice() {
return 567.8;
}
});
//调用无参数的构造器创建Device匿名实现类的对象
ai.test(new Device() {
//初始化块
{
System.out.println("匿名内部类的初始化块...");
}
//实现抽象方法
@Override
public double getPrice() {
return 66.6;
}
//重写父类的实例方法
@Override
public String getName() {
return "键盘";
}
});
}
}
创建匿名内部类时,必须实现接口或抽象父类里的所有抽象方法,如果有需要也可以重写父类中的普通方法。
在 Java 8 之前,被局部内部类、匿名内部类访问的局部变量必须使用 final 修饰。Java 8 中有“effectively final”,意思是对于被匿名内部类访问的局部变量可以用 final 修饰,也可以不用 final 修饰,但必须按照有 final 修饰的方式来使用,即不能重新赋值。
5.8 Lambda表达式
Lambda 表达式是 Java 8 的重要更新,它支持将代码块作为方法参数,允许使用更简洁的代码创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。
5.8.1 Lambda表达式基础
Lambda 表达代替匿名内部类创建对象时,不需要 new 接口名这种繁琐的代码,不需要指出重写方法名字,不需要给出重写的方法的返回值类型,只需要给出重写的方法括号以及括号内的形参列表。Lambda 表达式的代码块会代替实现抽象方法的方法体,相当于一个匿名方法,由以下三部分组成:
- 形参列表:形参列表允许省略形参类型,如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略;
- 箭头:
->
- 代码块:如果代码块只包含一条语句,Lambda 表达式允许省略代码块的括号;Lambda 代码块只有一条 return 语句,甚至可以省略 return 关键字。
public class LambdaTest {
//调用该方法需要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 a) {
System.out.println("5与3的和为" + a.add(5, 3));
}
public static void main(String[] args) {
LambdaTest lt = new LambdaTest();
lt.eat(() -> System.out.println("苹果味道很好"));
lt.drive(weather -> {
System.out.println("今天天气是" + weather);
System.out.println("直升机飞行平稳");
});
lt.test((a, b) -> 5 + 3);
}
}
5.8.2 函数式接口
Lambda 表达式的类型也被成为“目标类型”,Lambda 表达式的目标类型必须是函数式接口,即只含有一个抽象方法的接口,但接口可以包含多个默认方法、类方法。如果采用匿名内部类语法创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下用 Lambda 表达式来创建对象,对象的目标类型就是这个函数接口。
Java 8 专门为函数式接口提供了 @FunctionalInterface 注解,该注解通常放在接口定义前面,该注解对程序功能没有任何作用,用于告诉编译器执行更严格检查,即检查该接口必须是函数式接口,否则编译器报错。例如:Runnable、ActionListener 等接口都是函数式接口。
由于 Lambda 表达式的结果就是对象,因此程序中完全可以使用 Lambda 表达式进行赋值。
//Runnable接口中只包含一个无参数的方法
//Lambda表达式代表的匿名方法实现了Runnable接口中惟一的、无参数的方法
//因此下面Lambda表达式创建了Runnable对象
Runnable r = () -> {
for (int i = 0; i < 100; i++) {
System.out.println();
}
};
Lambda 表达式存在以下限制:
- Lambda 表达式的目标类型必须是明确的函数式接口;
- Lambda 表达式只能为函数式接口创建对象
因此,为保证 Lambda 表达式的目标类型是一个明确的函数式接口,有如下三种常见方式:
- 将 Lambda 表达式赋值给函数式接口类型的变量;
- 将 Lambda 表达式作为函数式接口类型的参数传给某个方法;
- 使用函数式接口对 Lambda 表达式进行强制类型转换;
Object obj = (Runnable)() -> {
for (int i = 0; i < 100; i++) {
System.out.println();
}
};
需要说明的是,同样的 Lambda表达式的目标类型完全可能是变化的,唯一要求是 Lambda 表达式实现的匿名方法与目标类型(函数式接口)中唯一的抽象方法有相同的形参列表。
Java 8 在 java.util.function 中定义了大量函数式接口,典型的包括以下4类接口:
- XxxFunction:包含一个 apply() 抽象方法,该方法对参数进行处理、转换然后返回一个新的值,常用于对指定数据进行转换处理;
- XxxConsumer:包含一个 accept() 抽象方法,该方法也负责对参数进行处理,但不会返回结果;
- XxxPredicate:包含一个 test() 抽象方法,该方法对参数进行某种判断返回一个 boolean 值,常用于筛选数据;
- XxxSupplier:包含一个 getAsXxx() 抽象方法,不需要输入参数,该方法会根据某种逻辑算法返回一个数据;
5.8.3 方法引用与构造器引用
如果Lambda 表达式的代码块只有一条代码,程序就可以省略 Lambda 表达式中代码的花括号,还可以在代码块中使用方法引用和构造器引用。Lambda 表达式支持的方法引用和构造器引用如下:
种类 | 示例 | 说明 | 对应的 Lambda表达式 |
---|---|---|---|
引用类方法 | 类名::类方法 | 函数式接口中被实现方法的全部参数传给该类方法作为参数 | (a,b,…) -> 类名.类方法(a,b,…) |
引用特定对象的实例方法 | 特定对象::实例方法 | 函数式接口中被实现方法的全部参数传给该方法作为参数 | (a,b,…) -> 特定对象.实例方法(a,b,…) |
引用某类对象的实例方法 | 类名::实例方法 | 函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数 | (a,b,…) -> a.实例方法(b,…) |
引用构造器 | 类名::new | 函数式接口中被实现方法的全部参数传给该构造器作为参数 | (a,b,…) -> new 类名(a,b,…) |
- 引用类方法
@FunctionalInterface
interface Converter {
Integer convert(String msg);
}
public class MethodRefer {
public static void main(String[] args) {
//Lambda表达式创建Converter对象,把代码的值作为返回值
Converter con1 = msg -> Integer.valueOf(msg);
//convert()方法执行体就是Lambda表达式的代码块部分
Integer val = con1.convert("99");
System.out.println(val);//输出整数99
//方法引用代替Lambda表达式:引用类方法
//函数式接口中被实现方法的全部参数传给该类方法作为参数
Converter con2 = Integer::valueOf;
}
}
- 引用特定的对象的实例方法
Converter con3 = msg -> "Lambda".indexOf(msg);
val = con3.convert("am");
System.out.println(val);//输出1
//方法引用代替Lambda表达式:引用特定对象的实例方法
//函数式接口中被实现方法的全部参数传给该方法作为参数
Converter con4 = "Lambda"::indexOf;
- 引用某类对象的实例方法
@FunctionalInterface
interface MyTest {
String test(String a, int b, int c);
}
public class MethodRefer {
public static void main(String[] args) {
MyTest mt1 = (a, b, c) -> a.substring(b, c);
String str = mt1.test("Hello, Java", 7, 10);
System.out.println(str);
//方法引用代替Lambda表达式:引用某类对象的实例方法
//函数式接口中被实现方法的第一个参数作为调用者
//后面参数全部传给该方法作为参数
MyTest mt2 = String::substring;
}
}
- 引用构造器
@FunctionalInterface
interface YourTest {
JFrame win(String title);
}
public class MethodRefer {
public static void main(String[] args) {
YourTest yt1 = (String a) -> new JFrame(a);
JFrame jf = yt1.win("我的窗口");
System.out.println(jf);
//构造器引用代替Lambda表达式
//函数式接口中被实现方法的全部参数传给该构造器作为参数
YourTest yt2 = JFrame::new;
}
}
5.8.4 Lambda表达式与匿名内部类
Lambda 表达式是匿名内部类的一种简化,因此可以部分取代匿名内部类的作用,Lambda 表达式与匿名内部类的相同点:
- Lambba 表达式与匿名内部类一样,都可以直接访问“Effectively final”的局部变量,以及外部列的成员变量(包括实例变量和类变量)。
- Lambda 表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
@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 = "Java";
public void test() {
String book = "Java学习笔记";
Displayable dp = () -> {
//访问”effectively final“局部变量
System.out.println(book);
//访问外部类的实例变量和类变量
System.out.println(age);
System.out.println(name);
};
dp.display();
//调用dp对象从接口中继承的add()方法
System.out.println(dp.add(3, 5));
}
public static void main(String[] args) {
LambdaAndInner lai = new LambdaAndInner();
lai.test();
}
}
Lambda 表达式与匿名内部类的不同点:
- 匿名内部类可以为任意接口创建实例,不管接口包含多少个实例方法,只要匿名内部类实现所有的抽象方法;但 Lambda 表达式只能为函数式接口创建实例;
- 匿名内部类可以为抽象方法甚至普通类创建实例;但 Lambda 表达式只能为函数式接口创建实例;
- 匿名内部类实现的抽象方法的方法体允许调用接口中的默认方法;但 Lambda 表达式的代码块不允许调用接口中定义的默认方法;
5.9 枚举类
一个类的对象是有限且固定的,在 Java 中被称为枚举类。
5.9.1 枚举类入门
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 访问控制符;
- 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例;列出这些枚举类时,系统会自动添加
public static final
修饰符; - 枚举类默认提供了一个 values() 方法,该方法可以遍历所有的枚举值;
public enum SeasonEnum {
//第一行列出4个枚举类实例
SPRING, SUMMER, FALL, WINTER;
}
编译以上 Java 程序,将生成SeasonEnum.class
文件,这表明枚举类是一个特殊的 Java 类。如果需要使用枚举类的某个实例,可以使用EnumClass.variable
的形式来访问。
public class EnumTest {
public void judge(SeasonEnum season) {
//switch语句里的表达式可以是枚举值
switch (season) {
case SPRING:
System.out.println("春暖花开");
break;
case SUMMER:
System.out.println("夏日炎炎");
break;
case FALL:
System.out.println("秋高气爽");
break;
case WINTER:
System.out.println("冬日飘雪");
break;
default:
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() | 该方法返回此枚举实例的名称 |
int ordinal() | 该方法返回枚举值在枚举类中的索引值 |
String toString() | 该方法返回枚举常量的名称(打印枚举类实例) |
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) | 该静态方法返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配 |
5.9.2 枚举类的成员
枚举类是一种特殊的类,因此也可以定义成员变量、方法和构造器。
public enum Gender {
MALE, FEMALE;
private String name;
public String getName() {
return this.name;
}
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 class GenderTest {
public static void main(String[] args) {
Gender gen = Gender.valueOf("FEMALE");
gen.setName("女");
System.out.println(gen + "代表" + gen.getName());
//此时设置name值将会输出参数错误
gen.setName("男");
System.out.println(gen + "代表" + gen.getName());
}
}
实际上这种做法依然不够好,枚举类通常应该设计成不可变类,如果将所有的成员变量都使用 final 修饰符来修饰,所以必须在构造里为这些成员变量指定初始值(或者在定义成员变量、初始化块中指定初始值,但这两种并不常见),因此应该为枚举类显式定义带参数的构造器。
public enum Gender
{
//此处的枚举值必须调用对应的构造器来创建
MALE("男"), FEMALE("女");
private final String name;
//枚举类的构造器只能使用private修饰
private Gender(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
}
5.9.3 实现接口的枚举类
枚举类可以实现一个或多个接口,也需要实现该接口所包含的方法。如果由枚举类来实现接口里的方法,则每个枚举值在调用方法时都有相同的行为方式。如果需要每个枚举值在调用该方法时呈现不同的行为方式,则可以让每个枚举值分别来实现该方法。
5.9.4 包含抽象方法的枚举类
枚举类里定义抽象方法时不能使用 abstract 关键字将枚举类定义成抽象类(因为系统会自动添加 abstract 关键字),但因为枚举类需显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则出现编译错误。