《疯狂Java讲义》学习笔记

第六章 面向对象(下)

6.1包装类

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
char Character
float Float
double Double
boolean Boolean

JDK1.5提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能,所谓自动装箱,就是可以把一个基本类型变量直接赋给对应的包装类变量,或者赋给Object变量(Object时所有类的父类,子类对象可以直接赋给父类变量);自动拆箱则相反,允许把一个包装类对象直接赋值给一个对应的基本类型变量。

进行自动装箱和自动拆箱时碧玺注意类型匹配,例如Integer只能自动拆箱成int类型变量,不要试图拆箱成boolean类型变量;与之类似,int类型变量也只能自动装箱成Integer对象(即使赋给Object类型变量,那也只是利用了Java的向上转型特性),不要试图装箱成为Boolean对象。

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;   //自动拆箱   Boolean转换为booleanSystem.out.println(b);}}}

Integer.parseInt()是把()里的内容转换成整数。

Integer. valueOf()可以将基本类型int转换为包装类型Integer,或者将String转换成Integer,String如果为Null或“”都会报错。

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 fdStr = String.valueOf(false);System.out.println(fdStr.toUpperCase());}}

系统把一个-128127之间的整数自动装箱成Integer实例,并放入一个名为cache的数组中缓存起来。如果以后把一个-128127之间的整数自动装箱成一个Integer实例时,实际上是直接指向对应的数组元素,因此-128127之间的同一个整数自动装箱成Integer实例时,永远都是引用cache数组的同一个数组元素,所以它们全部相等;但每次把一个不在-128127范围之内的整数自动装箱成Integer实例时,系统总会重新创建一个Integer实例,所以系统会判断不相等。

public class WrapperClassCompare
{public static void main(String[] args){var a = Integer.valueOf(6);//输出tureSystem.out.println("6的包装类实例是否大于5.0"+(a>5.0));//两个包装实例进行比较,只有两个包装类引用指向同一个对象时才会返回turnSystem.out.println("比较2个包装类的实例是否相等:"+ (Double.valueOf(2.0) == Double.valueOf(2.0)));//通过自动装箱,允许把基本类型值赋给包装类实例Integer ina = 2;Integer inb = 2;System.out.println("两个2自动装箱后是否相等:" + (ina == inb));Integer biga = 128;Integer bigb = 128;System.out.println("两个128自动装箱后是否相等:"+ (biga == bigb));System.out.println(Boolean.compare(true, false));}
}

Java 7为所有包装类都提供了一个静态的compare(xxx val1 , xxx val2)方法,来比较两个基本类型值的大小,包括两个Boolean类型值也可以,且ture>false。

Java 8 再次增强包装类的功能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U5AH8jdf-1612163756574)(D:\Program Files\Typora\Images\729c9218e91e590474c60c4f122ab6c.jpg)]

public class UnsignedTest
{public static void main(String[] args){byte b = -3;//将byte类型的-3转换为无符号整数System.out.println("byte类型的-3对应的无符号整数:"+ Byte.toUnsignedInt(b));//指定使用十六进制解析无符号整数var va1 = Integer.parseUnsignedInt("ab", 16);System.out.println(va1);//将-16转换成无符号int型,然后转换为十六进制的字符串System.out.println(Integer.toUnsignedString(-12,16));//将两个数转换成两个无符号整数后相除System.out.println(Integer.divideUnsigned(-2, 3));//将两个数转换为无符号整数相除后求余System.out.println(Integer.remainderUnsigned(-2, 7));}
}

无符号整数最大的特点是最高位不在被当成符号位,以-3为例,其源码是10000011,其反码是11111100,补码为11111101,如果将此数当成无符号整数处理,那么最高位的1就不再是符号位,也是数值位。

6.2处理对象

6.2.1打印对象和toString方法

class Person
{private String name;public Person(String name){this.name = name;//System.out.println(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()方法只能在控制台输出字符串,而Person实例时一个内存中的对象,怎么能直接转换为字符串输出呢?当使用该方法输出Person对象时,实际上输出的是Person对象的toString()方法的返回值,也就是说下面两行代码效果完全相同。

System.out.println(p);System.out.println(p.toString());
}

toString()方法是一个“自我描述”方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统将会输出该对象的“自我描述”信息,默认输出为“类名+@+hashCode”值,如果用户需要自定义类能实现“自我描述”的功能,就必需重写Ojbect类的toString()方法。

class Apple
{private String color;private double weight;public Apple() {  }//提供有参数的构造器public Apple(String color,double weight){this.color = color;this.weight = weight;}​ //重写toString()方法,用于实现Apple对象的“自我描述”
​   public String toString()
​   {​       return"一个苹果,颜色是:"+color+",重量是:"+weight;
​   }}
public class ToStringTest
{public static void main(String[] args){var a = new Apple("红色",5.68);//打印Apple对象System.out.println(a);  //实际输出的为a.toString()方法}
}

6.2.2 ==和equals方法

测试两个变量是否相等的两种方式:

== :如果两个变量是基本类型变量,且都是数值类型(不一定要求数据类型严格相同),则是要两个变量的值相等,就将返回ture。但对于两个引用类型变量。只有它们指向同一对象时,才会返回ture,它不可用于比较类型上没有父子关系的两个对象。==比较符也是比较指向的对象是否相同的也就是对象在对内存中的的首地址

public class EqualTest
{public static void main(String[] args){var it = 65;var fl = 65.0f;//将输出tureSystem.out.println("65和65.0f是否相等?"+(it == fl));var ch = 'A';//将输出tureSystem.out.println("65和'A'是否相等?"+(it == ch));var str1 = new String("Hello");var str2 = new String("Hello");//将输出falseSystem.out.println("str1和str2是否相等?"+ (str1 == str2));//输出trueSystem.out.println("str1是否equals str2?"+ (str1.equals(str2)));//由于java.lang.String与EqualTest类没有继承关系//所以下面语句导致编译错误//System.out.println("hello" == new EqualTest());}
}

new String(“hello”)时,JVM会先使用常量池来管理“hello”直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。换句话说,new String(“hello”)一共产生了两个字符串对象。

/常量池(constant pool)专门用于管理在编译时被确定并被保存在已编译的.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;//使用new调用构造器将会创建一个新的String对象//s7引用堆内存中新建的String对象var s7 = new String("疯狂Java");var s8 = new String("疯狂Java");System.out.println(s1 == s4);System.out.println(s1 == s5);System.out.println(s1 == s6);System.out.println(s1 == s7);System.out.println(s8 == s7);System.out.println(s8.equals(s7));}
true
true
false
false
false
true

s1,s4,s5所引用的字符串可以再编译期就确定下来,因此他们都将引用的字符串可以在编译器就确定下来,因此他们都将引用引用池中的同一个字符串对象。

使用new String()常见的字符串对象是运行时创建出来的,它被保存在运行时内存区(即堆内存)内,不会放入常量池。

equals()方法是Object类提供的一个实例方法,因此所有引用变量都可调用该方法来判断是否与其他引用变量变量相等,使用这个方法判断两个对象相等的标准与使用==没有什么区别,同样要求两个引用变量指向用一个对象才会返回ture。因此这个方法无太大意义,如果希望采用自定义的相等标准,则可采用重写equals方法实现。

equals()重写应该满足一下条件
  • 自反性:对任意x,x.equals(x)一定返回true。
  • 对称性:对任意x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。
  • 传递性:对任意x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true。
  • 一致性:对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么一直是ture,要么一直是false。
  • 对任何不是null的x,x.equals(null)一定返回false。

String已经重写了Object的equals()方法,String的equals()方法判断两个字符串是否相等的标准是:只要两个字符串所包含的字符串序列相同则可。

class Person8
{private String name;private String idStr;public Person8() {}public Person8(String name,String idStr){this.name = name;this.idStr = idStr;}//此处省略name和idStr的setter和getter方法public void setName(String name){this.name = name;}public String getName(){return this.name;}public void getIdStr(String idStr){this.idStr = idStr;}public String getIdStr(){return this.idStr;}//重写equalspublic boolean equals(Object obj){//如果两个对象同一个对象if(this == obj)return true;//只有当obj是Person对象//getClass() 返回此 Object 的运行时类//类名.class表示语法,表示Person这个类的类型信息if(obj != null && obj.getClass() == Person8.class){var personObj = (Person8) 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 Person8("孙悟空","123456789987");var p2 = new Person8("孙大圣","123456789987");var p3 = new Person8("齐天大圣","123654");//p1和p2的idStr相等,所以输出tureSystem.out.println("p1和p2是否相等?"+ p1.equals(p2));//p2和p3的idStr不相等,所以输出flaseSystem.out.println("p2和p3是否相等?"+ p2.equals(p3));}}

类名.class表示语法,表示Person这个类的类型信息

instanceof 是Java的一个二元操作符(运算符),也是Java的保留关键字。它的作用是判断其左边对象是否为其右边类的实例,返回的是boolean类型的数据。用它来判断某个对象是否是某个Class类的实例。(左边是对象,右边是类;当对象是右边类或子类所创建对象时,返回true;否则,返回false。)

6.3类成员

在Java类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)5种成员,其中static可以修饰成员变量,方法、初始化块、内部类(接口、枚举),以static修饰的成员就是类成员。类成员属于整个类。而不属于单个对象。

类变量的生存范围从系统第一次使用该类时开始,直到该类被卸载。几乎等同于该类的生存范围。当类初始化完成后,类变量也被初始化完成。

当通过对象来访问类变量时,系统会在底层转换为通过该类访问类变量。

**类成员不能访问实例成员(包括成员变量、方法、初始化块、内部类和内部枚举)。**因此类成员时属于类的,类成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还没初始化的情况,如果允许类成员访问实例变量将会引起大量错误。

6.3.2单例(Singleton)类

如果一个类始终只能创建一个实例,则这个类被称为单例类。

class Singleton
{//使用一个类变量来缓存曾经创建的实例private static Singleton instance;//隐藏构造器private Singleton(){}//提供一个静态方法,用于返回Singleton实例//该方法可以加入自定义控制,保证值产生一个Singleton对象public static Singleton getInstance(){if(instance == null){//创建一个对象,并将其缓存起来。instance = new Singleton();}return instance;}
}public class SingletonTest
{public static void main(String[] args){Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();//输出tureSystem.out.println(s1 ==s2);}
}

通过getInstance方法提供的自定义控制(这也是封装的又是;不允许自由访问类成员变量和实现细节,而正式通过方法来控制合适的暴露),保证类只能产生一个实例,所以在类的main方法种,看到两次产生的对象实际是同一对象.

6.4 final修饰符

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 = 9;}
//  静态初始化块,可对没有指定默认值的类变量指定初始值static{d = 5.6;}
//  构造器,可对既没有指定默认值,又没有在初始化块中指定初始值的实例变量指定初始值public FinalVariableTest(){//      如果初始化块中已经对str指定了初始值
//      那么在构造器中不能对final变量重新赋值,下面赋值语句非法
//      str = "World";c = 5;
//      ch = 'A';}public void changeFinal(){//      普通方法不能为final修饰的成员变量赋值
//      d = 5.1;
//      不能在普通方法中为final成员变量指定初始值
//      ch = 'S';}
//  public void shuchu()
//  {//      System.out.println(ch);
//  }public static void main(String[] args){FinalVariableTest v = new FinalVariableTest();
//      v.shuchu();System.out.println(v.a);System.out.println(v.c);System.out.println(v.d);}
}

与普通成员变量不同的是,final成员变量(包括实例变量和类变量)必须由程序员显式初始化。

如果打算在构造器或初始化块中对final成员变量进行初始化,则不要在初始化之前访问final成员变量;否则由于Java允许通过方法访问final成员变量,此时将看到系统将final成员变量默认初始化为0(或’\u0000’、false或null)的情况。


public class FinalErrorTest
{final int age;{//      系统不会对final成员变量进行默认初始化//java不允许在final成员变量显式初始化之前直接调用该成员变量,所以代码错误。
//      System.out.println(age);
//      因为Java允许方法来访问final修饰的成员变量,
//      此时系统将默认初始化,将输出0printAge();age = 6;System.out.println(age);}public void printAge(){System.out.println(age);}public static void main(String[] args){new FinalErrorTest();}
}

final局部变量

系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。

public class FinalLocalVariableTest
{public void test(final int a){//      不能对final修饰的形参赋值
//      a = 3;System.out.println("成功对a进行了初始化,且a的值为:"+ a);}public FinalLocalVariableTest() {}public static void main(String[] args){final var str = "Hello";
//      str = "Java";final double d;d = 5.6;
//      d = 3.4;FinalLocalVariableTest s = new FinalLocalVariableTest();s.test(8);}
}

因为形参在调用方法时,由系统根据传入的参数进行初始化,所以使用final修饰的形参不能被赋值。

6.4.3 final修饰基本类型变量和引用类型变量的区别

当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变,但对于引用类型变量而言,他保存的仅仅是一个引用,final只保证这个引用类型所引用的地址不变,即一直引用同一个对象,但这个对象完全可以发生改变。

import java.util.Arrays;class Personq
{private int age;public Personq() {}//有参数的构造器public Personq(int age){this.age = age;}public void setAge(int age){this.age = age;}public int getAge(){return 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;iArr[0] = 1;System.out.println(Arrays.toString(iArr));
//      下面语句对iArr重新赋值,非法
//      iArr = {2,6,4,8};
//      final 修饰Person变量,p时一个引用变量final var p = new Personq(45);
//      改变Person对象的age实例变量,合法p.setAge(23);System.out.println(p.getAge());
//      下面语句对p重新赋值,非法
//      p = null;}
}

从上可得,使用final修饰的引用类型变量不能被重新赋值,但可以改变引用类型变量所引用对象的内容。例如iArr变量所引用的数组对象,final修饰后的iArr变量不能被重新赋值,但是iArr所引用的数组元素可以被改变。相似的有,p变量不能被重新赋值,但是p变量所引用Person对象的成员变量的值可以被改变。

6.44 可执行“宏替换”的final变量

对于final变量来说,不管他是类变量、实例变量,还是局部变量,只要该变量满足三个条件,这个final变量就不再是一个变量,而是相当于一个直接量。

  • 使用final修饰符修饰。
  • 在定义该final变量时指定了初始值。
  • 该初始值可以在编译时就被确定下来。
public class FinalLocalTest
{public static void main(String[] args){final var a = 5;System.out.println(a);}
}

对于此程序,当程序执行System.out.println(a);,实际转换为执行System.out.println(5);这就是final修饰符的一个重要用途–宏定义,编译器会在程序中所有用到该变量的地方直接替换成该变量的值。

被赋值的可以是直接量,可以时算数表达式或字符串连接运算,没有访问普通变量,调用方法。

public class FinalReplaceTest
{public static void main(String[] args){final var a = 5 + 2;final var b = 1.2 / 3;//字符串连接运算final var c = "疯狂"+"Java";//字符串连接运算,隐式类型(将数值转字符串)final var d = "疯狂Java讲义:"+99.0;//调用了方法,无法在编译时被确定下来,无法当作宏变量处理final var d2 = "疯狂Java讲义:"+String.valueOf(99.0);System.out.println(d == "疯狂Java讲义:99.0");//因为java常量池,所以返true。System.out.println(d2 == "疯狂Java讲义:99.0");}
}
public class StringJoinTest
{public static void main(String[] args){var s1 = "疯狂Java";//两个字符串直接量连接,编译时即可确定类型var s2 = "疯狂" + "Java";System.out.println(s1 == s2);//turevar str1 = "疯狂";var str2 = "Java";//两个普通变量的运算,编译器不会执行“宏替换”var str3 = str1 + str2;System.out.println(s1 == str3);//false}
}

str1和str2只是两个普通变量,编译器不会执行“宏替换”,因此编译器无法在编译时确定s3的值,这是可以让编译器对两个变量执行“宏替换”(使用final修饰)。

6.4.5 final方法

final修饰的方法不可被重写,如果出于某些原因不希望子类重写父类的某个方法,则可以使用final修饰该方法。

public class FinalMethodTest
{public final void test() {}
}
class sub extends FinalMethodTest
{//编译会出错,不允许重写test方法//public  void test() {}
}

private修饰的final方法时,因为它仅在当前类可见,其子类无法访问该方法,所以子类无法重写该方法,如果子类出现同名函数,不是重写只是定义了一个新方法。

public class PrivateFinalMethodTest
{private final void test(){}
}
class Sub extends 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修饰这个类,使该类无法继承。

6.4.7 不可变类

不可变类的意思是创建该类的实例后,该实例的实例变量是不可改变的。Java提供的8个包装类和java.lang.String类都是不可变类,当创建他们的实例后,其实例的实例变量不可改变。

var d = Double.valueOf(6.5);
var str = new String("Hello");

如果需要创建自定义的不可变类,可遵守如下规则:

  • 使用private和final修饰符来修饰该类的成员变量。
  • 提供带参数的构造器(或返回该实例的类方法),用于根据传入参数来初始化类里的成员变量。
  • 仅为该类的成员变量提供getter方法,不要为该类的成员提供setter方法,因为普通方法无法修改final修饰的成员变量。
  • 如果有必要,重写Object类的hashCode()和equal()方法。equal()方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用equal()方法判断为相等对象的hashCode()也相等。
public class Address
{private final String detail;private final String postCode;
//  定义带参数的构造器public Address(String datail,String postCode){this.detail = datail;this.postCode = postCode;}
//  定义两个成员变量的getter方法public String getDetail(){return detail;}public String getPostCode(){return postCode;}
//  重写equals方法public boolean equlas(Object obj){if(this == obj){return true;}if(obj != null && obj.getClass() == Address.class){var ad = (Address)obj;if(this.getDetail().equals(ad.getDetail())&& this.getPostCode().equals(ad.getPostCode())){return true;}}return false;}public int hashCode(){return detail.hashCode() + postCode.hashCode() * 31;}
}

上面的Address类,当程序创建了Address对象后,同意无法修改该Address对象的detail和postCode实例变量的值。

当使用final修饰引用类型变量时,仅代表这个引用类型变量不可被重新赋值,但引用类型变量所指向的对象依然是可变的。这就产生了一个问题:当创建不可变类时,如果它包含成员变量的类型时可变的,那么其对象的成员变量的值依然是可改变的——这个不可变类其实是失败的。

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 Persom
{private final Name name;public Persom(Name name){this.name = name;}public Name getName(){return name;}public static void main(String[] args){var n = new Name("悟空","孙");var p = new Persom(n);System.out.println(p.getName().getFirstName());//改变Persom对象的name的firstName值。n.setFirstName("八戒");System.out.println(p.getName().getFirstName());System.out.println(n.getLastName()+n.getFirstName());}
}

当不可变类的实例变量引用了可变类的对象时,会被破坏设计不可变类的初衷。

因此对Persom类进行部分修改:

public class Persom
{private final Name name;public Persom(Name name){//      设置name实例变量为临时创建的Name对象,该对象的getFirstName和gerLastName
//      与传入的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());}

Persom类改写了设置name实例变量的方法,也改写了name的getter方法。当程序向Persom构造器里传入一个Name对象时,该构造器创建Persom对象时并不是直接利用已有的Name对象(利用已有的Name对象有风险,因为这个已有的Name对象是可变的,如果程序改变了这个Name对象,将会导致Persom对象也发生变化),而是重新创建了一个Name对象来赋给Persom对象的name实例变量。当Persom对象返回name变量时,他并没有直接把name实例变量返回,直接返回name实例变量的值也可能导致它所引用的Name对象被修改。

所以,创建不可变类要注意其引用类型的成员变量的类是否可变。

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设为1pos = 1;}else{//把新创建的对象缓存起来,pos加1cache[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");System.out.println(c1 == c2);}
}

CacheImmutale类使用一个数组来缓存该类的对象,这个数组长度为MAX_SIZE,即该类共可以缓存MAX_SIZE个CacheImmutale对象。当缓存池已满时,采用“先进先出(FIFO)”规则来决定哪个对象将被移出缓存池。

CacheImmutale类能控制系统生成CacheImmutale对象的个数,需要程序使用该类的valueOf()方法来得到其对象,而且程序使用private修饰符来隐藏该类的构造器,因此程序只能通过该类提供的valueOf()方法来获得实例。

Integer类构造器和valueOf方法存在的差异:

public class IntegerCacheTest
{public static void main(String[] args){//      生成新的Integer对象var in1 = new Integer(6);//生成新的并缓存该对象var in2 = Integer.valueOf(6);
//      直接从缓存池中取出 var in3 = Integer.valueOf(6);System.out.println(in1 == in2);System.out.println(in2 == in3);
//      由于Integer只缓存-128~127之间的值因此200对应的Integer对象没有缓存var in4 = Integer.valueOf(200);var in5 = Integer.valueOf(200);System.out.println(in4 == in5);}
}

6.5抽象类

6.5.1抽象方法和抽象类

有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。

  • 抽象方法和抽象类必须使用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.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 "三角形";}
}

普通类继承抽象类,普通类中必须实现抽象类中的抽象方法。

上面的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类的计算周长的抽象方法public double calPerimeter(){return 2*Math.PI*radius;}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());}
}
/*
输出结果为:
执行Shape的初始化块·····         //参考5.9.3类初始化块。p163
执行Shape的构造器·····
执行Shape的初始化块·····
执行Shape的构造器·····
三角形
12.0
黄色圆形
18.84955592153876
*/

上面main()方法中定义了两个Shape类型的引用变量,它们分别指向Triangle对象和Circle对象,由于在Shape类中定义了calPerimeter()方法和getType()方法,所以程序可以直接调用s1变量和s2变量的calPerimeter()方法和getType()方法,无需强制转换为子类类型。

利用抽象类和抽象方法的优势,可以更好地发挥多态的优势,使得程序更加灵活。

当使用abstract修饰类时,表明这个类智能被继承;当使用abstract需要事故方法时,表明这个方法须由自雷实现(即重写)。而fianl修饰的类不能被继承,final修饰的方法不能被重写,因此final和abstract永远不能同时使用。

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()方法必须推迟到其子类中实现。

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());}
}

SpeedMeter类里提供了速度表的通用算法,但一些具体的实现细节则推迟到其子类CarSpeedMeter类中实现。这也是一种典型的模板模式。

,抽象类里定义的构造器只能是普通构造器。

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()方法必须推迟到其子类中实现。

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());}
}

SpeedMeter类里提供了速度表的通用算法,但一些具体的实现细节则推迟到其子类CarSpeedMeter类中实现。这也是一种典型的模板模式。

《疯狂Java讲义》学习笔记 第六章 面向对象(下)相关推荐

  1. 《疯狂Java讲义》学习笔记 第六章 面向对象(下续)

    这里## 6.6 Java 9改进的接口 6.6.1 接口的概念 6.6.2 Java 9中接口的定义 6.6.3接口的继承 ==以下代码中纯在自己的很多错误== 6.6.4使用接口 6.6.5接口和 ...

  2. 《疯狂的JAVA讲义》笔记-第8章集合

    <疯狂的JAVA讲义>笔记-第8章集合 Iterator .ListIterator接口 Iterator迭代时是将元素的值返回,并不是将元素本身返回,所以迭代时无法更改元素的值.但是可以 ...

  3. Unix原理与应用学习笔记----第六章 文件的基本属性2

    Unix原理与应用学习笔记----第六章 文件的基本属性2 改变文件权限命令:chmod 提示:文件或目录创建后,就被赋予一组默认的权限.所有的用户都有读,只有文件的所有者才有写. 相对权限设置 Ch ...

  4. 《Go语言圣经》学习笔记 第六章 方法

    <Go语言圣经>学习笔记 第六章 方法 目录 方法声明 基于指针对象的方法 通过嵌入结构体来扩展类型 方法值和方法表达式 示例:Bit数组 封装 注:学习<Go语言圣经>笔记, ...

  5. Java基础学习——第十六章 Java8新特性

    Java基础学习--第十六章 Java8 新特性 Java8(JDK8.0)较 JDK7.0 有很多变化或者说是优化,比如 interface 里可以有静态方法和默认方法,并且可以有方法体,这一点就颠 ...

  6. 疯狂Android讲义 - 学习笔记(二)

    疯狂Android讲义 - 学习笔记(二) Android应用的用户界面编程 2.1 界面编程与视图(View)组件 Android应用的绝大部分UI组件放在android.widget.androi ...

  7. 疯狂Kotlin讲义学习笔记04-05章:流程控制、数组和集合

    1.when分支取代swith分支 不在需要使用case关键字 case后面的冒号改为-> default改为更有意义的else 如果一个case里有多条语句,需要将多条语句用大括号括起来 wh ...

  8. 疯狂Kotlin讲义学习笔记07章:面向对象(上)对象,中缀,解构,幕后字段、属性,延迟初始化,访问控制符,构造器,继承,重写,super限定,重写,多态,is类型检查,as强制类型转换

    1.定义类的标准格式 修饰符 class 类名 [ constructor 主构造器]{零到多个次构造器定义零到多个属性....零到多个方法.... } 修饰符open是final的反义词,用于修饰一 ...

  9. 疯狂python讲义学习笔记——中十章完结

    #第十一章 thinker import tkinter as tk print(help(tk.Button.__init__))#以按扭为例查看有什么属性 class myApplication( ...

最新文章

  1. 模仿国外某小哥,做的一个字符串转动态linq表达式 及 部分扩展
  2. 使用TESSERACT来识别字符
  3. 【PAT (Advanced Level) Practice】1002 A+B for Polynomials (25 分)
  4. 王爽汇编第二册:将每个单词的前四位改为大写字母
  5. 终于有人把AI、BI、大数据、数据科学讲明白了
  6. 千呼万唤始出来,OpenCV 4.0正式发布!
  7. jupyter notebook使用入门2——创建一个基于scikit-Learn的线性预测ipynb文件
  8. 《DSP using MATLAB》示例Example4.6
  9. 批量修改所选文件夹中所有文件的名称
  10. linux多人共享桌面,gnome 开远程桌面共享功能(linux启动多个桌面)
  11. python化学公式配平_PYTHON趣用—配平化学方程式-阿里云开发者社区
  12. Excel 技巧篇 - 选择性粘贴表格数据,excel只粘贴数值不粘贴公式
  13. 原生的APP、小程序(微信小程序、支付宝小程序、头条小程序、百度小程序.等等)、H5 的优势与劣势分析有那些?
  14. 百度地图api使用时标注图标显示不出来
  15. dspbios设计指南_视频广告设计者指南
  16. 软文营销有什么效果,主要作用是什么?
  17. Unity-Live2d(表情系统,姿势动作与口型功能的实现)
  18. MeterSphere案例分享丨88完美邮箱全面提升产品质量的落地指南
  19. main()的使用说明 (一叶知秋)
  20. 新闻推荐数据集MIND介绍

热门文章

  1. 英语钻石法则(一)-----句子中心论
  2. sbc8600_还记得我复制的价值8600万美元的车牌扫描仪吗? 我抓住了它。
  3. 唯品会财报:一面骄阳,一面寒霜
  4. (七)fastai 2018 lesson8 目标检测 ~ lesson9 目标检测
  5. 轻松6步完成App开发
  6. 个人开发者与企业开发者的区别
  7. 选品指南:波兰市场什么最好卖?有哪些热门类目?
  8. 好男儿当生三国 好女子当养唐朝
  9. 服务器显示屏 超出工作频率范围,WIN7电脑显示器超出工作频率范围的处理方法...
  10. 嵌入式Linux工程师的成长经历