Java讲义第六章学习笔记
chapter 6 面向对象(下)
6.1 包装类
基本数据类型 | 包装类 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
自动装箱:就是可以把一个基本类型变量直接赋给对应的包装类变量,或者赋给Object变量(Object是所有类的父亲,子类对象可以直接赋给父类变量);
自动拆箱:允许直接把包装类对象直接赋给一个对应的基本类型变量。
public class AutoBoxingUnboxing { public static void main(String[] args) { //直接把一个基本类型变量赋值给Integer对象 Integer inObj = 5; //直接把一个boolean类型变量赋值给一个Object类型的变量 Object boolObj = true; //直接把一个Integer对象赋给int类型的变量 int it = inObj; if(boolObj instanceof Boolean){ //先把Object对象强制类型转换为Boolean类型,再赋值给Boolean变量 boolean b = (Boolean) boolObj; System.out.println(b); } } }
当JDK提供了自动装箱和自动拆箱功能后,大大简化了基本类型变量和包装类对象之间的转换过程。值得提出的是,进行自动装箱和自动拆箱时必须注意类型匹配。
除此之外,包装类还可以实现基本类型变量和字符串之间的转换。把字符串类型的值转换为基本类型的值有两种方式。
①利用包装类提供的parseXxx(String s)静态方法(除Character之外的所有包装类都提供了该方法。)
②利用包装类提供的ValueOf(String s)静态方法。
public class Primitive2String{ public static void main(String[] args) { var intStr = "123"; //把一个特定字符串转换成int变量 var it1 = Integer.parseInt(intStr); var it2 = Integer.valueOf(intStr); System.out.println(it2); var floatStr = "4.56"; //把一个特定字符串转换成float变量 var ft1 = Float.parseFloat(floatStr); var ft2 = Float.valueOf(floatStr); System.out.println(ft2); //把一个float变量转换成String变量 var ftStr = String.valueOf(2.345f); System.out.println(ftStr); //把一个double变量转换成String变量 var dbStr = String.valueOf(3.344); System.out.println(dbStr); //把一个boolean变量转换成String变量 var boolStr = String.valueOf(true); System.out.println(boolStr.toUpperCase()); //转大写 } }
包装类的无符号算数运算功能展示:
public class UnsignedTest{ public static void main(String[] args) { byte b = -3; //将byte类型的-3转换为无符号整数 System.out.println("byte 类型的-3对应的无符号整数:" +Byte.toUnsignedInt(b)); //指定使用十六进制解析无符号整数 var val = Integer.parseUnsignedInt("ab",16); System.out.println(val); //输出171 //将-12转换为无符号int型,然后转换为十六进制字符串 System.out.println(Integer.toUnsignedString(-12,16)); //输出fffffff4 //将两个数转换为无符号整数后相除 System.out.println(Integer.divideUnsigned(-2,3)); //1431655764 //将两个数转换为无符整数相除后取余 System.out.println(Integer.remainderUnsigned(-2,7)); //2 } }
6.2 处理对象
--6.2.1 打印对象和toString方法
class Person{ private String name; public Person(String name){ this.name = name; } } public class PrintObject{ public static void main(String[] args) { //创建一个Person对象,将之赋给p变量 var p = new Person("孙悟空"); //打印p所引用的Person对象 System.out.println(p); //等价于System.out.println(p.toString()); } }
Object类提供的toString()方法总是返回该对象实现类的”类名+@+hashCode"值,这个返回值并不能真正实现”自我描述“的功能,因此如果用户需要自定义类实现”自我描述“的功能,就必须重写Object类的toString()方法。常用格式:
类名[field1 = 值1,field2 = 值2,...]
--6.2.2 ==和equals 方法
==:如果两个变量是基本类型变量,且都是数值类型(不一定要求数据类型严格相同)(字符类型则去ASCII码),则只要两个变量的值相等,就将返回true。
但对于两个引用类型变量,只有它们指向同一个对象时,==判断才会返回true。==不可用于比较类型上没有父子关系的两个对象。
提示: 常量池专门用于管理在编译时被确定并被保存在已编译的 .class文件中的一些数据。
下面程序示范了JVM使用常量池管理字符串直接量的情形。
public class StringCompareTest{ public static void main(String[] args) { //s1直接引用常量池中的“疯狂Java"; var s1 = "疯狂Java"; var s2 = "疯狂"; var s3 = "Java"; //s4后面的字符串值可以在编译时就确定下来 //s4直接引用常量池中的"疯狂Java" var s4 = "疯狂" + "Java"; //s5后面的字符串值可以在编译时就确定下来 //s5直接引用常量池中的"疯狂Java" var s5 = "疯" + "狂" + "Java"; //s6后面的字符串值不能在编译时就确定下来 //不能引用常量池中的字符串 var s6 = s2 + s3; //s2,s3会以对象的形式复制到堆内存 //使用new调用构造器将会创建一个新的String对象 //s7 引用堆内存中新创建的String对象 var s7 = new String("疯狂Java"); System.out.println(s1 == s4); //输出true System.out.println(s1 == s5); //输出true System.out.println(s1 == s6); //输出false System.out.println(s1 == s7); //输出false } }
JVM常量池保证相同的字符串直接量只有一个,不会产生多个副本。
equal()方法是Object类提供的一个实例方法,因此所有引用变量都可调用该方法来判断是否与其他引用变量相等。但使用这个方法判断两个对象相等的标准与使用==运算符没有区别,同样要求两个引用变量指向同一个对象才会返回true.因此这个Object类提供equals()方法没有太大的实际意义,如果希望采用自定义的相等标准,则可采用重写equals方法来实现。
class Person{ private String name; private String idStr; public Person(){} public Person(String name,String idStr){ this.name = name; this.idStr = idStr; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getIdStr() { return idStr; } public void setIdStr(String idStr) { this.idStr = idStr; } //重写equals() public boolean equals(Object obj){ //如果两个对象为同一个对象 if(this == obj) return true; //只有当obj是Person对象 if(obj != null && obj.getClass() == Person.class){ var personObj = (Person) obj; //并且当前对象的idStr与obj对象的idStr相等时才可判断两个对象相等 if(this.getIdStr().equals(personObj.getIdStr())){ return true; } } return false; } } public class OverrideEqualsRight{ public static void main(String[] args) { var p1 = new Person("孙悟空","12343433433"); var p2 = new Person("孙行者","12343433433"); var p3 = new Person("孙悟饭","99933433"); // p1和p2的idStr相等,所以输出true System.out.println("p1和p2是否相等?" + p1.equals(p2)); // p2和p3的idStr不相等,所以输出false System.out.println("p2和p3是否相等?" + p2.equals(p3)); } }
正确的重写equals应满足:
1、自反性:x.equals(x)返回true;
2、对称性:若x.equals(y)为true,则y.equals(x)亦为true;
3、传递性:若x.equals(y)为true且y.equals(z)也为true,则x.equals(z)亦为true;
4、一致性:x.equals(y)的第一次调用为true,那么x.equals(y)的第二次、第三次、第n次调用也均为true,前提条件是没有修改x也没有修改y;
5、对于非空引用x,x.equals(null)返回为false。
6.3 类成员
static 关键字修饰的成员就是类成员,前面已经介绍的类成员有类变量、类方法、静态初始化块三个部分,static 关键字不能修饰构造器。static修饰的类成员属于整个类,不属于单个实例。
--6.3.1 理解类成员
在Java类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)5种成员。
类变量属于整个类,当系统第一次准备使用该类时,系统会为该类变量分配内存空间,类变量开始生效,直到该类被卸载,该类的类变量所占有的内存才被系统的垃圾回收机制回收。
类变量既可通过类来访问,也可通过类的对象来访问。但通过类的对象来访问类变量时,实际上并不是访问该对象所拥有的变量,因为当系统创建该类对象时,系统不会为类变量分配内存,也不会再次对类变量进行初始化,对象根本不拥有对应类的类变量。可以这样理解:当通过对象来访问类变量时,系统会在底层转换为通过该类来访问类变量。
当使用实例来访问类成员时,实际上依然是委托给该类来访问类成员,因此即使某个实例为null,它也可以访问它所属类的类成员。
public class NullAccessStatic{ public static void test(){ System.out.println("static修饰的类方法"); } public static void main(String[] args) { //定义一个NullAccessStatic变量,其值为null NullAccessStatic nas = null; //使用null对象调用所属类的静态方法 nas.test(); } }
编译、运行上面的程序,一切正常,程序将打印出“static修饰的类方法"字符串,这表明null对象可以访问它所属类的类成员。
类初始化块也是类成员的一种,类初始化块用于执行类初始化动作。一旦该类初始化结束后,类初始化块将永远不会获得执行的机会。、
--6.3.2 单例类
如果一个类始终只能创建一个实例,则这个类被称为单例类。
总之,在一些特殊场景下,要求不允许自由创建该类的对象,而只允许为该类创建一个对象。为了避免其他类自由创建该类的实例,应该把该类的构造器使用private修饰,从而把该类的所有构造器隐藏起来。
根据良好封装的原则:一旦把该类的构造器隐藏起来,就需要提供一个public方法作为该类的访问点,用于创建该类的对象,且该方法必须使用static修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)。
除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾将创建过对象,也就无法保证只创建一个对象。为此该类需要使用一个成员变量来保存曾经的对象,因为该成员变量需要被上面的静态方法访问,故该成员变量必须使用static修饰。
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); //输出true,保证是同一个对象 } }
6.4 final修饰符
final 关键字可用于修饰类、变量和方法,用于表示修饰的类、方法和变量不可改变。final修饰变量时,表示该变量一旦获得了初始值就不可被改变,不能被重新赋值。
--6.4.1 final成员变量
final修饰的成员变量必须由程序员显示的指定初始值。
归纳起来,final修饰的类变量、实例变量能指定初始值的地方如下。
类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方其中之一指定。
实例变量:必须在非静态初始化块,声明该实例变量或构造器中指定初始值,而且只能在三地方的其中之一指定。
public class FinalVariableTest{ //定义成员变量时指定默认值,合法 final int a = 6; //下面变量将在构造器或初始化块中分配初始值 final String str; final int c; final static double d; //既没有指定默认值,又没有在初始化块、构造器中指定初始值 //下面定义实例变量是不合法的 //final char ch; //初始化块,可对没有指定默认值的实例变量指定初始值 { //在初始化块中为实例变量指定初始值,合法 str = "Hello"; //定义a实例变量时已经指定默认值 //不能为a重新赋值,因此下面赋值语句非法 //a = 9; } //静态初始化块,可对没有指定默认值的类变量指定初始值 static { //在静态初始化块中为类变量指定初始值,合法 d = 5.6; } //构造器,可对既没有默认值,又没有在初始化块中 //指定初始值的实例变量指定初始值 public FinalVariableTest(){ c = 5; } public void changeFinal(){ //普通方法不能为final修饰的成员变量赋值 //d = 1.2; //不能在普通方法中为final成员变量指定初始值 //ch = 'a'; } public static void main(String[] args){ var ft = new FinalVariableTest(); System.out.println(ft.a); System.out.println(ft.c); System.out.println(ft.d); } }
final成员变量默认初始化初始值为0。
public class FinalErrorTest{ //定义一个final修饰的实例变量 //系统不会对final成员变量进行默认初始化 final int age; { //age没有初始化,所以此处代码将引起错误 // System.out.println(age); printAge();//这行代码是合法的,程序输出0,但违背了final成员变量设计初衷。 age = 6; } public void printAge(){ System.out.println(age); } public static void main(String[] args) { new FinalErrorTest(); } }
Java不允许在final成员变量显式初始化之前,直接通过final修饰的age成员变量。
--6.4.2 final 局部变量
系统不会对局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。
public class FinalLocalVariableTest{ public void test(final int a){ //不能对final修饰的形参赋值,下面语句非法 //a=5; } public static void main(String[] args) { //定义final局部变量时指定默认值,则str变量无法重新赋值 final var str = "hello"; //下面赋值语句非法 //str = "java"; //定义final局部变量时没有指定默认值,则d变量可被赋值一次 final double d; d = 5.6; //对final变量重复赋值,下面语句非法 //d = 3.4; } }
--6.4.3 final修饰基本类型变量和引用类型变量的区别
当使用final修饰基本数据类型变量时,不能对基本类型变量重新赋值,因此基本类型变量被改变。但对于引用类型变量而言,他保存的仅仅是一个引用,final只保存这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
class Person { private int age; public Person() { } //有参数的构造器 public Person(int age) { this.age = age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } public class FinalReferenceTest{ public static void main(String[] args) { //final修饰数组变量,iArr是一个引用变量 final int[] iArr = {5,6,12,9}; System.out.println(Arrays.toString(iArr)); //对数组元素进行排序,合法 Arrays.sort(iArr); System.out.println(Arrays.toString(iArr)); //对数组元素赋值,合法 iArr[2] = -8; System.out.println(Arrays.toString(iArr)); //下面语句对iArr重新赋值,非法 //iArr = null; //final修饰Person变量,p是一个引用变量 final var p = new Person(45); //改变Person对象的age实例变量,合法 p.setAge(23); System.out.println(p.getAge()); //对p重新赋值,非法。 //p = null; } }
--6.4.4 可执行“宏替换”的final变量
final变量作为宏变量条件:
1.使用final变量修饰符修饰
2.在定义该final变量时指定了初始值
3.该初始值可以在编译时就被确定下来
public class StringJoinTest { public static void main(String[] args) { var s1 = "疯狂Java"; //s2 变量引用的字符串可以在编译时就确定下来 //因此s2直接引用常量池中已有的"疯狂Java"字符串 var s2 = "疯狂" + "Java"; System.out.println(s1 ==s2); //true //定义两个字符串直接量 var str1 = "疯狂"; //① var str2 = "Java"; //② //将str1 和 str2 进行连接运算 var s3 = str1 + str2; System.out.println(s1 == s3);//false } }
s2的值是两个字符串直接量进行连接运算,由于编译器可以在编译阶段就确定s2为 "疯狂Java",所以系统会让s2直接指向系统常量池缓存中的 "疯狂Java"字符串,因此返回true;
对于s3,它的值由str1和str2进行连接运算后得到。由于str1、str2只是两个普通变量,编译器不会执行"宏替换”,因此编译器无法在编译时确定s3的值,也就无法让s3指向字符串中缓存的"疯狂Java"。所以输出false。
让s1 == s3 输出true也可以,只要让编译器可以对str1,str2两个变量进行"宏替换",这样编译器即可在编译阶段就确定s3的值。所以,只要将①,②两行代码所定义的str1,str2使用final修饰即可。
--6.4.5 final方法
final 修饰的方法不可以被重写,如果出于某些原因,不希望子类重写父类的某个方法,则可以使用final修饰该方法。
Java提供的Object类里就有一个final方法:getClass(),因为Java不希望任何类重写这个方法,所以使用final把这个方法密封起来。但对于该类提供的toString()和equals()方法,都允许子类重写,因此没有用final修饰。
下面使用public修饰的final方法,试图重写将引发编译错误。
public class FinalMethodTest{ public final void test(){} } class Sub extends FinalMethodTest{ //下面方法定义将出现编译错误,不能重写final方法 public void test(){} }
如果使用private修饰final方法,private对子类隐藏该方法,故可以重新定义它。
public class PrivateFinalMethodTest{ private final PrivateFinalMethodTest{ //下面方法定义则不会出现问题 public void test(){} }
final修饰的方法仅仅是不能被重写,并不是不能被重载,因此,下面程序完全没问题。
public class FinalOverload{ //final修饰的方法只是不能被重写,完全可以被重载 public final void test(){ } public final void test(String arg) }
--6.4.6 final类
final修饰的类不可以有子类,例如java.lang.Math类就是一个final类,他不可以有子类。
当子类继承父类时,将可以访问到父类内部数据,并可以通过重写父类方法来实现细节,这可能导致一些不安全的因素。为了保证某个类是不可继承的,则可以使用final来修饰这个类。
public final class FinalClass{} //下面的类定义将出现编译错误 class Sub extends FinalClass{}
--6.4.7 不可变类
不可变类的意思是创建该类的实例后,该实例的实例变量是不可变的。Java提供的8个包装类和java.lang.String类都是不可变类。当创建他们的实例后,其实例的实例变量不可改变。
创建自定义不可变类需要遵守的规则:
1、使用private和final修饰成员变量。
2、提供带参构造方法,用于初始化成员变量。
3、不要为成员变量提供setter方法,仅提供getter方法。
4、如果成员变量中有可变类时需要重写Object中的hashCode方法和equals方法。
java.lang.String这个类就做的很好,他就是根据String对象里的字符序列来作为相等的标准,其hashCode()方法也是根据字符序列计算得到的。
public class ImmutableStringTest{ public static void main(String[] args) { var str1 = new String("Hello"); var str2 = new String("Hello"); System.out.println(str1 == str2); //false System.out.println(str1.equals(str2)); //true //下面输出的hashCode相同 System.out.println(str1.hashCode()); System.out.println(str2.hashCode()); } }
下面定义一个不可变的Address类,程序把Address类的detail和postCode成员变量都使用private隐藏起来,并使用final修饰这两个成员变量,不允许其他方法修改这两个成员变量的值。
public class Address{ private String detail; private String postCode; //在构造器里初始化两个实例变量 public Address(String detail,String postCode){ this.detail = detail; this.postCode = postCode; } //仅为两个实例变量提供getter方法 public String getDetail() { return detail; } public String getPostCode() { return postCode; } //重写equals()方法,判断两个对象是否相等 public boolean equals(Object obj){ if(this == obj){ return true; } if(obj != null && obj.getClass() == Address.class) { var ad = (Address) obj; //当detail和postCode相等时,可认为两个Address对象相等 if(this.getDetail().equals(ad.getDetail()) && this.getClass().equals(ad.getPostCode())){ return true; } } return false; } public int hashCode(){ return detail.hashCode() + postCode.hashCode() + 31; } }
下面程序试图定义一个不可变类的Person类,但因为Person类包含一个引用类型的成员变量,且这个引用类是可变类,所以导致Person类也变成了可变类。
class Name { private String firstName; private String lastName; public Name(){} public Name(String firstName, String lastName){ this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } public class Person{ private final Name name; public Person(Name name){ this.name = name; } public Name getName(){ return name; } public static void main(String[] args) { var n = new Name("悟空","孙"); var p = new Person(n); //Person对象的name的firstName值为"悟空" System.out.println(p.getName().getFirstName()); //改变Person对象的name的firstName值 n.setFirstName("八戒"); //Person对象的name的firstName值被改为"八戒" System.out.println(p.getName().getFirstName()); } }
为了保持Person对象的不可变性,必须保护好Person对象的引用类型的成员变量:name,让程序无法访问到Person对象的name成员变量,也就无法利用name成员变量的可变性来改变对象了。下面为改进处理:
public class Person{ private final Name name; public Person(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()); } }
--6.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 name; } public static CacheImmutale valueOf(String name) { //遍历已缓存对象。 for(var 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){ var 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) { var c1 = CacheImmutale.valueOf("hello"); var c2 = CacheImmutale.valueOf("hello"); //输出true System.out.println(c1 == c2); } }
6.5 抽象类
--6.5.1 抽象方法和抽象类
抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。
抽象方法和抽象类规则如下:
- 抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。
- 抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。
- 抽象类可以包含成员变量、方法、构造器、初始化块内部类,5种成分。抽象类的构造器不能用于实例创建,主要用于被其子类调用。
- 含有抽象方法的类,只能被定义为抽象类。
定义一个Shape抽象类:
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对象时被调用的,而是在创建其子类的实例时被调用。
下面定义一个三角形类,三角形类被定义成普通类,因此必须实现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 "三角形"; } }
注意:abstract 不能用于修饰成员变量,不能用于修饰局部变量,既没有抽象变量没有抽象成员变量等说法;abstract也不能用于修饰构造器,没有抽象构造器,抽象类里定义的构造器只能是普通构造器。
除此之外,当使用static修饰一个方法时,表明这个方法属于该类本身,即通过类就可调用该方法。但如果该方法被定义成抽象方法,则将导致通过该类来调用该方法时出现错误。因此static和abstract不能同时修饰某个方法,即没有所谓的抽象方法。
注意:static 和 abstract 并不是绝对互斥的,static 和 abstract 虽然不能同时修饰某个方法,但他们可以同时修饰内部类。
注意:abstract 关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此 abstract 方法不能定义为 private 访问权限,即 private 和 abstract 不能同时修饰方法。
--6.5.2 抽象类的作用
模板设计:
public abstract class SpeedMeter{ //转速 private double turnRate; public SpeedMeter(){} //把计算车轮周长的方法定义成抽象方法 public abstract double calGirth(); public void setTurnRate(double turnRate){ this.turnRate = turnRate; } //定义计算速度的通用算法 public double getSpeed(){ //速度等于 周长 * 转速 return calGirth() * turnRate; } }
上面程序定义了一个抽象的SpeedMeter类(车速表),该表里定义了一个getSpeed()方法,该方法用于返回当前车速,getSpeed()方法依赖于calGirth()方法的返回值。对于一个抽象的SpeedMeter类而言,他无法确定车轮的周长,因此calGirth()方法必须推迟到子类实现。
下面是其子类 CarSpeedMeter 的代码,该类实现了其抽象父类的calGirth()方法,即可以创建 CarSpeedMeter 类的对象,也可以通过该对象来获取当前速度。
public class CarSpeedMeter extends SpeedMeter{ private double radius; public CarSpeedMeter(double radius){ this.radius = radius; } public double calGirth(){ return radius * 2 * Math.PI; } public static void main(String[] args) { var csm = new CarSpeedMeter(0.34); csm.setTurnRate(15); System.out.println(csm.getSpeed()); } }
模板模式(Template ):
模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。
模式中的角色:
抽象类(AbstractClass):实现了模板方法,定义了算法的骨架。
6.6 Java 9 改进的接口
--6.6.1 接口的概念
接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这就意味着接口里通常是定义一组公用的方法。
--6.6.2 Java 9 中接口的定义
和类定义不同,定义接口不再使用 class 关键字,而是使用 interface 关键字。
[修饰符] interface 接口名 extend 父接口1,父接口2... { 零到多个常量定义... 零到多个抽象方法定义... 零到多个内部类、接口、枚举 定义... 零到多个私有方法、默认方法、类方法 定义... }
➢修饰符可以是public或者省略,如果省略了public访问控制符,则默认采用包权限访问控制符。
➢接口名应与类名采用相同的命名规则
➢一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
public interface Output{ //接口里定义成员变量只能是常量 int MAX_SIZE_CACHE_LINE = 50; //接口里定义的普通方法只能是public的抽象方法 void out(); void getData(String msg); //在接口中定义默认方法,需要使用default修饰 default void print(String...msgs){ for (var msg : msgs){ System.out.println(msg); } } //在接口中定义默认方法,需要使用default修饰 default void test(){ System.out.println("默认的test()方法"); } //在接口中定义类方法,需要使用static修饰 static String staticTest() { return "接口里的类方法"; } //定义私有方法 private void foo(){ System.out.println("foo私有方法"); } //定义私有静态方法 private static void bar(){ System.out.println("bar私有静态方法"); } }
--6.6.3 接口的继承
接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量。一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以英文逗号(,)隔开。
接口的继承程序如下:
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); } }
--6.6.4 使用接口
接口主要有如下用途:
➢ 定义变量,也可用于进行强制类型转换。
➢ 调用接口中定义的常量。
➢ 被其他类实现。
一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法;否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类。
--6.6.5 接口和抽象类
接口和抽象类很像,它们都具有如下特征:
➢ 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
➢ 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
接口和抽象类在用法上也存在如下差别:
➢ 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则可以包含普通方法。
➢ 接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量。
➢ 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
➢ 接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
➢ 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口。
➢ 接口是一种规范,抽象类是模板模式。
--6.6.6 面向接口编程 (设计模式)
6.7 内部类
Java从JDK 1.1开始引入内部类,内部类主要有如下作用:
➢ 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
➢ 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的 成员变量。
➢ 匿名内部类适合用于创建那些仅需要一次使用的类。
从语法角度来看,定义内部类与定义外部类的语法大致相同,内部类除需要定义在其他类里面之外,还存在如下两点区别:
➢ 内部类比外部类可以多使用三个修饰符:private、protected、static—外部类不可以使用这三个修饰符。
➢ 非静态内部类不能拥有静态成员。
--6.7.1 非静态内部类
内部类都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类则不是类成员。
成员内部类分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类是非静态内部类。
成员内部类(包括静态内部类、非静态内部类)的class文件总是这种形式:OuterClass$InnerClass.class。
当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量;如果不存在,则到该方法所在的内部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果不存在,则到该内部类所在的外部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果依然不存在,系统将出现编译错误:提示找不到该变量。因此,如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用this、外部类类名.this作为限定来区分。
非静态内部类的成员可以访问外部类的实例成员,但反过来就不成立了。如果外部类需要访问非静态内部类的实例成员,则必须显式创建非静态内部类对象来调用访问其实例成员。
根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外部类的静态成员中直接使用非静态内部类。
Java不允许在非静态内部类里定义静态成员,非静态内部类里不能有静态方法、静态成员变量、静态初始化块。
public class Outer{ private int outProp = 9; class Inner{ private int inProp = 5; public void accessOuterProp(){ //非静态内部类可以直接访问外部类的private实例变量 System.out.println(outProp); } } public void accessInnerProp(){ //外部类不能直接访问非静态内部类的实例变量 //下面代码出现编译错误 //System.out.println(inProp); //如果访问内部类的实例变量。必须显示创建内部类对象 System.out.println(new Inner().inProp); } public static void main(String[] args) { //执行下面代码,只创建了外部类对象,还未创建内部类对象 var out = new Outer(); out.accessInnerProp(); } }
--6.7.2 静态内部类
静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。
--6.7.3 使用内部类
1.在外部类内部使用内部类
从前面程序中可以看出,在外部类内部使用内部类时,与平常使用普通类没有太大的区别。一样可以直接通过内部类类名来定义变量,通过new调用内部类构造器来创建实例。唯一存在的一个区别是:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。
2.在外部类以外使用非静态内部类
如果希望在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。
对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用:
➢ 省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类所访问。
➢ 使用protected修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类所访问。
➢ 使用public修饰的内部类,可以在任何地方被访问。
在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:
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("测试信息"); /* 上面代码可改为如下三行代码 使用OuterClass.InnerClass的形式定义内部类变量 Out.In in; 创建外部类实例,非静态内部类实例将寄生在该实例中 Out out = new Out(); 通过外部类实例和 new 来调用内部类构造器创建非静态内部类实例 in = out.new In("测试信息"); */ } }
3.在外部类以外使用静态内部类
因为静态内部类是外部类类相关的,因此创建静态内部类对象时无须创建外部类对象。在外部类以外的地方创建静态内部类实例的语法如下:
new OuterClass.InnerContructor(); //StaticOut.StaticIn in = new StaticOut.StaticIn();
--6.7.4 局部内部类
如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符修饰。
public class LocalInnerClass{ public static void main(String[] args) { //定义局部内部类 class InnerBase{ int a; } //定义局部内部类的子类 class InnerSub extends InnerBase{ int b; } //创建局部内部类的对象 var is = new InnerSub(); is.a = 5; is.b = 8; System.out.println(is.a+","+is.b); } }
--6.7.5 匿名内部类
匿名内部类适合创建那种只需要一次使用的类,定义匿名内部类的格式如下:
new 实现接口{} | 父类构造器{实参列表} { //匿名内部类的类体成分 }
关于匿名内部类还有如下两条规则:
➢ 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
➢ 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。
interface Product{ double getPrice(); String getName(); } public class AnonyousTest{ public void test(Product p){ System.out.println("购买了一个" + p.getName() + ", 花掉了" + p.getPrice()); } public static void main(String[] args) { var ta = new AnonyousTest(); //调用test()方法时,需要传入一个Product参数 //此处传入其匿名实现类的实例 ta.test(new Product() { @Override public double getPrice() { return 567.8; } @Override public String getName(){ return "AGP 显卡"; } }); } }
6.8 Java11增强的Lambda表达式
--6.8.1 Lambda 表达式入门
当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块将会代替实现抽象方法的方法体,Lambda表达式就相当一个匿名方法。
Lambda表达式的主要作用就是代替匿名内部类的烦琐语法,它由三部分组成:
➢ 形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
➢ 箭头(->)。必须通过英文中画线和大于符号组成。
➢ 代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。Lambda代码块只有一条return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而它的代码块中仅有一条省略了return的语句,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) { var 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)); } }
--6.8.2 Lambda表达式与函数接口
Lambda表达式的类型,也被称为“目标类型(target type)”,Lambda表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
由于Lambda表达式的结果就是被当成对象,因此程序可以使用Lambda表达式进行赋值。
//Runnable 接口中只包含一个无参数的方法 //Lambda 表达式代表的匿名方法实现了Runnable接口中唯一的、无参数方法 //因此下面的 Lambda 表达式创建了一个Runnable对象 Runnable r = () -> { //Runnable是Java本身提供的一个函数接口。 for(var i = 0;i < 100;i++){ System.out.println(); } };
Lambda表达式实现的是匿名方法——因此它只能实现特定函数式接口中的唯一方法。这意味着Lambda表达式有如下两个限制。
1.Lambda表达式的目标类型必须是明确的函数式接口
2.Lambda表达式只能为函数式接口创建对象
Object obj = () -> { for(var i = 0;i < 100;i++){ System.out.println(); } };
由于Object并不是函数式接口,故代码会报错。为了保证Lambda表达式的目标类型是一个明确的函数式接口,可有如下三种常见方式。
1.将Lambda表达式赋值给函数式接口类型的变量。
2.将Lambda表达式作为函数式接口类型的参数传给某方法。
3.使用函数式接口对Lambda表达式进行强制类型转换。
Object obj =(Runnable)() -> { //Runnable是Java本身提供的一个函数接口。 //强制转换 for(var i = 0;i < 100;i++){ System.out.println(); } };
@FunctionalInterface为自定义函数接口注解
//自定义函数接口 @FunctionalInterface interface FKTest{ void run(); } Object obj2 =(FKTest)() -> { //Runnable是Java本身提供的一个函数接口。 for(var i = 0;i < 100;i++){ System.out.println(); } };
--6.8.3 在Lambda表达式中使用var
...
--6.8.4 方法引用和构造器引用
前面已经介绍过,如果Lambda表达式的代码块只有一条代码,程序就可以省略Lambda表达式中代码块的花括号。不仅如此,如果Lambda表达式的代码块只有一条代码,还可以在代码块中使用方法引用和构造器引用。方法引用和构造器引用可以让Lambda表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号。Lambda表达式支持如表所示的几种引用方式:
--6.8.5 Lambda 表达式与匿名内部类的联系和区别
Lambda表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用。
Lambda表达式与匿名内部类存在如下相同点。
➢ Lambda表达式与匿名内部类一样,都可以直接访问“effectively final”的局部变量,以及外部类的成员变量(包括实例变量和类变量)。
➢ Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
Lambda表达式与匿名内部类主要存在如下区别。
➢ 匿名内部类可以为任意接口创建实例———不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但Lambda表达式只能为函数式接口创建实例。
➢ 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。
➢ 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但Lambda表达式的代码块不允许调用接口中定义的默认方法。
6.9 枚举类
--6.9.1 手动实现枚举类
在早期代码中,可能会直接使用简单的静态常量来表示枚举:
public static final SEASON_SPRING = 1; public static final SEASON_SUMMER = 2; public static final SEASON_FALL = 3; public static final SEASON_WINTER = 4;
这种定义方法简单明了,但存在几个问题。
- 类型不安全:因为上面的每个季节实际上一个int整数,因此完全可以把一个季节当成一个int整数使用。
- 没有命名空间:当需要使用季节时,必须在SPRING之前使用SEASON_前缀,否则程序可能与其它类中的静态常量混淆。
- 打印输出的意义不明确:当输出某个季节时,例如输出SEASON_SPRING,实际上输出的是1,这个1很难猜测到它代表春天。
--6.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修饰符。由于枚举类的所有构造器都是private的,而子类构造器总要调用父类构造器一次,因此枚举类不能派生子类。
➢ 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加。枚举类默认提供了一个values()方法,该方法可以很方便地遍历所有的枚举值。
public enum SeasonEnum{ // 在第一行列4个枚举实例 SPRING,SUMMER,FALL,WINTER; }
编译上面Java程序,将生成一个SeasonEnum.class文件,这表明枚举类是一个特殊的Java类。
定义枚举类时,需要显示列出所有枚举值,如上面的SPRING,SUMMER,FALL,WINTER;所示,所有枚举值之间以英文逗号(,)隔开,枚举值列举结束后以英文分号作为结束。
这些枚举值代表了该枚举类的所有可能实例。
--6.9.3 枚举类的成员变量、方法和构造器
枚举类也是一种类,只是它是一种比较特殊的类,因此它可以定义成员变量、方法和构造器。下面程序将定义一个Gender枚举类。
public enum Gender{ MALE,FEMALE; //定义一个public修饰的实例变量 public String name; }
上面的Gender枚举类里定义了一个名为name的实例变量,并将它定义成一个public访问权限的。下面通过如下程序来使用该枚举类。
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枚举类时与使用一个普通类没有太大的差别,差别只是产生Gender对象的方式不同,枚举类的实例只能是枚举值,而不是随意的通过new来创建枚举类对象。
正如前面提到,Java应该把所有设计成良好封装的类,所以不应该允许直接访问Gender类的name成员变量,而是应该通过方法来控制对name的访问。
public enum Gender{ MALE,FEMALE; private String 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 String getName(){ return this.name; } }
上面程序把name设置成private,从而避免其他程序直接访问该name成员变量,必须通过setName()方法来修改Gender实例的name变量,而setName()方法就可以保证不会产生混乱。
看下面程序:
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()); } }
上面修改name的值,系统会提示参数报错。
实际上这种做法依然不够好,枚举类通常应该设计成不可变类,也就是说,他的成员变量值不应该允许改变。因此建议将枚举类的成员变量都使用private final修饰。
如果将所有的成员变量都使用了final修饰符来修饰,所以必须在构造器里为这些成员变量指定初始值。因此应该为枚举类显示定义带参数构造器。
public enum Gender{ //此处的枚举值必须调用对应的构造器来创建 MALE("男"),FEMALE("女"); private final String name; //枚举类的构造器只能使用private修饰 private Gender(String name){ this.name = name; } public String getName() { return name; } }
从上面的程序可以看出,当为Gender枚举类创建了一个Gender(String name)构造器之后,列出枚举值就应该采用粗体字代码来完成。也就是说,在枚举类中列出枚举值时,实际上就是构造器创建枚举类对象,只是这里无需使用 new 关键字,也无需显式调用构造器。前面列出枚举值时无需传入参数,甚至无需使用括号,仅仅是因为前面的枚举类包含无参数的构造器。
不难看出,上面程序中粗体字代码实际上等同于如下两行代码:
public static final Gender MALE = new Gender("男“); public static final Gender FEMALE = new Gender("女“);
--6.9.4 实现接口的枚举类
枚举类也可以实现一个或多个接口,与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口时,也需要实现该接口所包含的方法。
public interface GenderDesc{ void info(); }
public enum Gender implements GenderDesc{ ... public void info(){ //实现接口方法 ... } }
--6.9.5 包含抽象方法和枚举类
public enum Operation{ PLUS{ public double eval(double x,double y){ return x + y; } }, MINUX{ 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; } }; //为枚举类定义一个抽象方法 //这个抽象方法由不同的枚举值提供不同的实现 public abstract double eval (double x,double y); public static void main(String[] args) { //.... } }
枚举类里定义抽象方法时不能使用abstract关键字将枚举类定义成抽象类,但因为枚举类需显示创建枚举类,而不是作为父类,所以定义每个枚举类时必须为抽象方法提供实现,否则出现编译错误。
6.10 对象与垃圾回收
--6.10.1 对象在内存中的状态
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:
➢ 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。
➢ 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的finalize()方法进行资源清理。如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态。
➢ 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize()方法后依然没有使该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
--6.10.2 强制垃圾回收
当一个对象失去引用后,系统何时调用它的finalize()方法对它进行资源清理,何时它会变成不可达状态,系统何时回收它所占有的内存,对于程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,绝不能控制它何时被回收。程序无法精确控制Java垃圾回收的时机,但依然可以强制系统进行垃圾回收—这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。大部分时候,程序强制系统垃圾回收后总会有一些效果。
强制系统垃圾回收有如下两种方式:
➢ 调用System类的gc()静态方法:System.gc()。
➢ 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()。
这种强制只是建议系统立即进行垃圾回收,系统完全有可能并不立即进行垃圾回收,垃圾回收机制也不会对程序的建议完全置之不理:垃圾回收机制会在收到通知后,尽快进行垃圾回收。
--6.10.3 finalize方法
finalize()方法具有如下4个特点。
➢ 永远不要主动调用某个对象的finalize()方法,该方法应交给垃圾回收机制调用。
➢ finalize()方法何时被调用,是否被调用具有不确定性,不要把finalize()方法当成一定会被执行的方法。
➢ 当JVM执行可恢复对象的finalize()方法时,可能使该对象或系统中其他对象重新变成可达状态。
➢ 当JVM执行finalize()方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。
--6.10.4 对象的软、弱和虚引用
Java语言对对象的引用有如下4种方式:
1.强引用(StrongReference)这是Java程序中最常见的引用方式。程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象,前面介绍的对象和数组都采用了这种强引用的方式。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。
2.软引用(SoftReference)软引用需要通过SoftReference类来实现,当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。
3.弱引用(WeakReference)弱引用通过WeakReference类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收—正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。
4.虚引用(PhantomReference)虚引用通过PhantomReference类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。
弱引用:
public class ReferenceTest{ public static void main(String[] args) throws Exception{ //创建一个字符串对象 var str = new String("疯狂Java讲义"); //创建一个弱引用,让此弱引用引用到"疯狂Java讲义"字符串 var wr = new WeakReference(str); //1 //切断str引用和"疯狂Java讲义"字符串之间的引用 str = null; //2 //取出弱引用所引用的对象 System.out.println(wr.get()); //3 未被回收 //强制垃圾回收 System.gc(); System.runFinalization(); //再次取出弱引用所引用的对象 System.out.println(wr.get()); //已被回收 } }
结果输出:
1 疯狂Java讲义 2 null
虚引用:
public class PhantomReferenceTest{ public static void main(String[] args) throws Exception{ //创建一个字符串对象 var str = new String("疯狂Java讲义"); //创建一个引用队列 var rq = new ReferenceQueue(); //创建一个虚引用,让此虚引用引用到"疯狂Java讲义"字符串 var pr = new PhantomReference(str,rq); //切断str引用和"疯狂Java讲义"字符串之间的引用 str = null; //取出虚引用的对象,并不能通过虚引用获取被引用的对象,所以此处输出null System.out.println(pr.get()); //1 //强制垃圾回收 System.gc(); System.runFinalization(); //垃圾回收之后,虚引用将被放入引用队列中 //取出引用队列中最先进入队列的引用与pr进行比较 System.out.println(rq.poll() == pr); //2 } }
输出结果:
1 null 2 true