疯狂Java讲义(六)----第一部分

 本章要点:

  • JDK1.5提供了自动装箱和自动拆箱功能,允许把基本类型值直接赋给对应的包装类引用变量,也允许把包装类对象直接赋给对应的基本类型变量。
  • Java提供了final 关键字来修饰变量、方法和类,系统不允许为final变量重新赋值,子类不允许覆盖父类的 final方法,final类不能派生子类。通过使用final关键字,允许Java实现不可变类,不可变类会让系统更加安全。
  • abstract和 interface两个关键字分别用于定义抽象类接口,抽象类和接口都是从多个子类中抽象出来的共同特征。但抽象类主要作为多个类的模板,而接口则定义了多类应该遵守的规范。Lambda表达式是Java 8的重要更新,本章将会详细介绍Lambda表达式的相关内容。enum关键字用于创建枚举类,枚举类是一种不能自由创建对象的类,枚举类的对象在定义类时已经固定下来。枚举类特别适合定义像行星、季节这样的类它们能创建的实例是有限且确定的

1. Java 8 增强的包装类

        Java是面向对象的编程语言,但它也包含了8种基本数据类型,这8种基本数据类型不支持面向对象的编程机制,基本数据类型的数据也不具备“对象”的特性:没有成员变量、方法可以被调用。Java之所以提供这8种基本数据类型,主要是为了照顾程序员的传统习惯
        这8种基本数据类型带来了一定的方便性,例如可以进行简单、有效的常规数据处理。但在某些时候,基本数据类型会有一些制约,例如所有引用类型的变量都继承了Object 类,都可当成Object类型变量使用。但基本数据类型的变量就不可以,如果有个方法需要Object类型的参数,但实际需要的值却是2、3等数值,这可能就比较难以处理。
        为了解决8种基本数据类型的变量不能当成Object类型变量使用的问题,Java提供了包装类(Wrapper Class)的概念,为8种基本数据类型分别定义了相应的引用类型,并称之为基本数据类型的包装类。

        

        在JDK 1.5以前,把基本数据类型变量变成包装类实例需要通过对应包装类的 valueOf()静态方法来实现。在JDK 1.5以前,如果希望获得包装类对象中包装的基本类型变量,则可以使用包装类提供的xxxValue()实例方法。由于这种用法已经过时,故此处不再给出示例代码。
        通过上面介绍不难看出,基本类型变量和包装类对象之间的转换关系如图6.1所示。

         从图6.1中可以看出,Java提供的基本类型变量和包装类对象之间的转换有点烦琐,但从JDK 1.5之后这种烦琐就消除了,JDK 1.5提供了自动装箱((Autoboxing〉和自动拆箱(AutoUnboxing)功能所谓自动装箱,就是可以把一个基本类型变量直接赋给对应的包装类变量,或者赋给Object变量(Object是所有类的父类,子类对象可以直接赋给父类变量);自动拆箱则与之相反,允许直接把包装类对象直接赋给一个对应的基本类型变量。


        当JDK提供了自动装箱和自动拆箱功能后,大大简化了基本类型变量和包装类对象之间的转换过程。值得指出的是,进行自动装箱和自动拆箱时必须注意类型匹配,例如Integer 只能自动拆箱成int类型变量,不要试图拆箱成boolean类型变量;与之类似的是,int类型变量只能自动装箱成Integer对象(即使赋给Object类型变量,那也只是利用了Java的向上自动转型特性),不要试图装箱成Boolean对象。
        借助于包装类的帮助,再加上JDK 1.5提供的自动装箱、自动拆箱功能,开发者可以把基本类型的变量“近似”地当成对象使用(所有装箱、拆箱过程都由系统自动完成,无须程序员理会);反过来,开发者也可以把包装类的实例近似地当成基本类型的变量使用。

         除此之外,包装类还可实现基本类型变量和字符串之间的转换。把字符串类型的值转换为基本类型的值有两种方式。

➢利用包装类提供的parseXx(String s)静态方法(除Character 之外的所有包装类都提供了该方法。
➢利用包装类提供的valueOf(String s)静态方法
String类也提供了多个重载valueOf(方法,用于将基本类型变量转换成字符串,下面程序示范了这种转换关系。

 

 

         此处要指出的是,虽然包装类型的变量是引用数据类型,但包装类的实例可以与数值类型的值进行比较,这种比较是直接取出包装类实例所包装的数值来进行比较的。

         两个包装类的实例进行比较的情况就比较复杂,因为包装类的实例实际上是引用类型,只有两个包装类引用指向同一个对象时才会返回true。下面代码示范 了这种效果(程序清单同上)。 

         但JDK1.5以后支持所谓的自动装箱,自动装箱就是可以直接把一个基本类型值赋给一个包装类实例,在这种情况下可能会出现一些特别的情形。看如下代码(程序清单同上)。

         上面程序让人比较费解:同样是两个int类型的数值自动装箱成Integer实例后,如果是两个2自动装箱后就相等;但如果是两个128自动装箱后就不相等,这是为什么呢?这与Java的Integer类的设计有关,查看Java系统中java.lang.Integer类的源代码,如下所示。

         从上面代码可以看出,系统把一个 -128~127之间的整数自动装箱成Integer实例,并放入了一个名为cache的数组中缓存起来。如果以后把一个- 128~127之间的整数自动装箱成一个Integer实例时,实际上是直接指向对应的数组元素,因此-128~127之间的同一个整数自动装箱成Integer实例时,永远都是引用cache数组的同一个数组元素,所以它们全部相等;但每次把一个不在-128~127范围内的整数自动装箱成Integer实例时,系统总是重新创建--个Integer实例,所以出现程序中的运行结果。

         Java 7增强了包装类的功能,Java 7为所有的包装类都提供了一个静态的compare(xxx vall, xxx val2)方法,这样开发者就可以通过包装类提供的compare(xxx vall, xxx val2)方法来比较两个基本类型值的大小,包括比较两个boolean类型值,两个boolean类型值进行比较时,true>false。例如如下代码:

 2.打印对象和toString方法

         上面程序创建了一个 Person对象,然后使用System.out.println()方法输出Person 对象。编译、运行上面程序,看到如下运行结果: .

         当使用该方法输出Person对象时,实际上输出的是Person对象的toString()方法的返回值。也就是说,下面两行代码的效果完全一样。

         toString()方法是Object类里的一个实例方法,所有的Java类都是Object类的子类,因此所有的Java对象都具有toString()方法。

        不仅如此,所有的Java对象都可以和字符串进行连接运算,当Java对象和字符串进行连接运算时,系统自动调用Java对象toString()方法的返回值和字符串进行连接运算,即下面两行代码的结果也完全相同。

       大部分时候,重写toString()方法总是返回该对象的所有令人感兴趣的信息所组成的字符串。通常可返回如下格式的字符串:

 因此,可以将上面Apple类的toString()方法改为如下:

3. ==和equals方法

 == :

        当使用==来判断两个变量是否相等时,如果两个变量是基本类型变量且都是数值类型(不一定要求数据类型严格相同),则只要两个变量的值相等,就将返回true。
        但对于两个引用类型变量,只有它们指向同一个对象时,==判断才会返回true。 ==不可用于比较类型上没有父子关系的两个对象。下面程序示范了使用==来判断两种类型变量是否相等的结果。

 运行上面程序,可以看到65、65.0f 和'A'相等。但对于str1 和str2,因为它们都是引用类型变量,它们分别指向两个通过new关键字创建的String对象,因此str1和str2两个变量不相等。

        对初学者而言,String 还有一个非常容易迷惑的地方: "hello" 直接量和new String("hello")有什么区别呢?

        当Java程序直接使用形如"hello"的字符串直接量(包括可以在编译时就计算出来的字符串值)时,JVM将会使用常量池来管理这些字符串;当使用new String("hello")时,JVM会先使用常量池来管理"hello"直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。换句话说,new String("hello")一共产生了两个字符串对象

 下面程序示范了JVM使用常量池管理字符串直接量的情形。

         JVM常量池保证相同的字符串直接量只有一个,不会产生多个副本。例子中的s1、s4、 s5 所引用的字符串可以在编译期就确定下来,因此它们都将引用常量池中的同一个字符串对象。
        使用newString(创建的字符串对象是运行时创建出来的它被保存在运行时内存区(即堆内存)内,不会放入常量池中。

equals():

        但在很多时候, 程序判断两个引用变量是否相等时,也希望有一种类似于 “值相等”的判断规则,并不严格要求两个引用变量指向同一个对象。例如对于两个字符串变量,可能只是要求它们引用字符串对象里包含的字符序列相同即可认为相等。此时就可以利用String对象的equals()方法来进行判断,例如.上面程序中的str1.equals(tr2)将返回true。
 

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

通常而言,正确地重写equals()方法应该满足下列条件。
自反性:对任意x,x.equals(x)一定返回true。
对称性:对任意x和y,如果y.equals(x)返回true, 则x.equals(y)也返回true。
传递性:对任意x, y, z,如果x.equals(y)返回ture, y.equals(z)返回true, 则x.equals(z)一定返回
true。
一致性:对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)
多少次,返回的结果应该保持一致,要么一直是true,要么一直是 false.
对任何不是null的x,x.equals(null)一 定返回false。
        Object默认提供的equals()只 是比较对象的地址,即Object类的equals0方法比较的结果与==运算符比较的结果完全相同。因此,在实际应用中常常需要重写equals()方法,重写equals方法时,相等条件是由业务要求决定的,因此equals()方法的实现也是由业务要求决定的。

4.类成员

        当使用实例来访问类成员时实际上依然是委托给该类来访问类成员,因此即使某个实例为null,它也可以访问它所属类的类成员。例如如下代码:

 

         对static 关键字而言,有一条非常 重要的规则:类成员(包括方法、初始化块、内部类和枚举类)不能访问实例成员(包括成员变量、方法、初始化块、内部类和枚举类)。因为类成员是属于类的,类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。

5.单例(Singleton)类

如果一个类始终只能创建一 个实例,则这个类被称为单例类。应该把该类的构造器使用private修饰,从而把改类的所有构造器隐藏起来。
        根据良好封装的原则:一旦把 该类的构造器隐藏起来,就需要提供一个 public方法作为该类的访问点,用于创建该类的对象,且该方法必须使用static修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)
        除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过对象,也就无法保证只创建一个对象。为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,故该成员变量必须使用static 修饰。

 6. final修饰符

        final关键字可用于修饰类、变量和方法,用于表示它修饰的类、方法和变量不可改变
        final修饰变量时,表示该变量一旦获得了初始值就不可被改变,final 既可以修饰成员变量(包括类变量和实例变量),也可以修饰局部变量、形参。有的书上介绍说final修饰的变量不能被赋值,这种说法是错误的!严格的说法是,final 修饰的变量不可被改变,一旦获得了初始值,该final变量的值就不能被重新赋值
        由于final变量获得初始值之后不能被重新赋值,因此final修饰成员变量和修饰局部变量时有一定的不同。

  (1)final成员变量

        成员变量是随类初始化或对象初始化而初始化的。当类初始化时,系统会为该类的类变量分配内存,并分配默认值;当创建对象时,系统会为该对象的实例变量分配内存,并分配默认值。也就是说,当执行静态初始化块时可以对类变量赋初始值;当执行普通初始化块、构造器时可对实例变量赋初始值。因此,成员变量的初始值可以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。

     对于final 修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、'\u0000'、 false 或null, 这些成员变量也就完全失去了存在的意义。因此Java语法规定: final 修饰的成员变量必须由程序员显式地指定初始值

归纳起来,final 修饰的类变量、实例变量能指定初始值的地方如下。
类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。.
实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能在三个地方的其中之一指定。

        如果打算在构造器、初始化块中对final成员变量进行初始化,则不要在初始化之前直接访问final成员变量;但Java又允许通过方法来访问final成员变量,此时会看到系统将final成员变量默认初始化为0 ( 或'\u0000'、false 或null)。例如如下示例程序。

  (2) final局部变量

        系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值

        如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该final变量赋初始值,但只能一次, 不能重复赋值;如果final 修饰的局部变量在定义时已经指定默认值,则后面代码中不能再对该变量赋值。下面程序示范了final修饰局部变量、形参的情形。

         在上面程序中还示范了final 修饰形参的情形。因为形参在调用该方法时,由系统根据传入的参数来完成初始化,因此使用final修饰的形参不能被赋值

  (3) final修饰基本类型变量和引用类型变量的区别

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

         从上面程序中可以看出,使用final修饰的引用类型变量不能被重新赋值但可以改变引用类型变量所引用对象的内容。例如上面iArr变量所引用的数组对象,final修饰后的iArr变量不能被重新赋值,但iArr所引用数组的数组元素可以被改变。与此类似的是,p变量也使用了final 修饰,表明p变量不能被重新赋值,但p变量所引用Person对象的成员变量的值可以被改变。

  (4) 可执行“宏替换”的final变量

        对一个final 变量来说,不管它是类变量、实例变量,还是局部变量,只要该变量满足三个条件,这个final变量就不再是一个变量,而是相当于一个直接量
        ➢使用final修饰符修饰。
        ➢在定义该final变量时指定了初始值。
        ➢该初始值可以在编译时就被确定下来。.

       上面程序中的粗体字代码定义了一个final局部变量,并在定义该final变量时指定初始值为5。对于这个程序来说,变量a其实根本不存在,当程序执行System.out.println(a);代码时, 实际转换为执行System.out.println(5)。

         除上面那种为final 变量赋值时赋直接量的情况外,如果被赋的表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量,调用方法,Java 编译器同样会将这种final 变量当成“宏变量”处理。示例如下。

         上面程序中粗体字代码定义了4个final变量,程序为这4个变量赋初始值指定的初始值要么是算术表达式,要么是字符串连接运算。即使字符串连接运算中包含隐式类型(将数值转换为字符串)转换,编译器依然可以在编译时就确定a、b、str、 book 这4个变量的值,因此它们都是“宏变量”。
        从表面上看,①行代码定义的book2与book没有太大的区别,只是定义book2变量时显式将数值99.0转换为字符串,但由于该变量的值需要调用String类的方法,因此编译器无法在编译时确定book2的值,book2 不会被当成“宏变量”处理。
        程序最后两行代码分别判断book、book2 和“疯狂Java讲义: 99.0” 是否相等。由于book是一个“宏变量”,它将被直接替换成“疯狂Java讲义: 99.0”, 因此book和“疯狂Java讲义: 99.0” 相等,但book2和该字符串不相等。

 

         让s1==s3输出true 也很简单,只要让编译器可以对strl、str2 两个变量执行“宏替换”,这样编译器即可在编译阶段就确定s3的值,就会让s3指向字符串池中缓存的“疯狂Java”。也就是说,只要将①、②两行代码所定义的str1、 str2使用final修饰即可

  (5) final 方法

        final修饰的方法不可被重写,如果出于某些原因,不希望子类重写父类的某个方法,则可以使用final修饰该方法。
        Java提供的Object类里就有一个final 方法: getClass(), 因为Java不希望任何类重写这个方法,所以使用final把这个方法密封起来。但对于该类提供的toString(和equalsQ方法,都允许子类重写,因此没有使用final 修饰它们。

         对于一个private 方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法一如果子类中定 义一个与父类private 方法有相同方法名、相同形参列表、相同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。因此,即使使用final修饰-个private访问权限的方法,依然可以在其子类中定义与该方法具有相同方法名、相同形参列表、相同返回值类型的方法。
        下面程序示范了如何在子类中“重写”父类的private final方法。

final修饰的方法仅仅是不能被重写,并不是不能被重载。

  (6) final类

final修饰的类不可以有子类,例如java.lang.Math类就是一个final 类,它不可以有子类。
 

  (7) 不可变类

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

         上面程序创建 了一个Double对象和一个 String对象,并为这个两对象传入了6.5和"Hello"字符串作为参数,那么Double类和String类肯定需要提供实例变量来保存这两个参数,但程序无法修改这两个实例变量的值,因此Double类和String类没有提供修改它们的方法

如果需要创建自定义的不可变类,可遵守如下规则。
        ➢使用private和final修饰符来修饰该类的成员变量。
        ➢提供带参数构造器,用于根据传入参数来初始化类里的成员变量。
        ➢仅为该类的成员变量提供getter方法不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量。
        ➢如果有必要,重写Object类的hashCode()和equals()方法(关于重写hashCode()的步骤可参考8.3.1节)。equals()方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等。

        例如,javalang.String这个类就做得很好,它就是根据String对象里的字符序列来作为相等的标准,其hashCode()方法也是根据字符序列计算得到的。下面程序测试了java.lang.String类的equals()和hashCode()方法。


 

         下面定义一个不可变的Address类,程序把Address类的detail和postCode成员变量都使用private隐藏起来,并使用final修饰这两个成员变量,不允许其他方法修改这两个成员变量的值。

 与可变类相比,不可变类的实例在整个生命周期中永远处于初始化状态,它的实例变量不可改变。因此对不可变类的实例的控制将更加简单

        前面介绍final 关键字时提到,当使用final 修饰引用类型变量时,仅表示这个引用类型变量不可被重新赋值,但引用类型变量所指向的对象依然可改变。这就产生了一个问题:当创建不可变类时,如果它包含成员变量的类型是可变的,那么其对象的成员变量的值依然是可改变的----这个不可变类其实是失败的
       下面程序试图定义一个不可变的Person 类,但因为Person类包含一个引用类型的成员变量,且这个引用类是可变类,所以导致Person类也变成了可变类

         为了保持Person对象的不可变性,必须保护好Person对象的引用类型的成员变量: name, 让程序无法访问到Person对象的name成员变量,也就无法利用name成员变量的可变性来改变Person对象了。为此将Person类改为如下:

 没有返回传进来的实体变量!!!

  (8) 缓存实例的不可变类

        不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。毕竟重复创建相同的对象没有太大的意义,而且加大系统开销。如果可能,应该将已经创建的不可变类的实例进行缓存。
        缓存是软件设计中一个非常有用的模式,缓存的实现方式有很多种,不同的实现方式可能存在较大的性能差别,关于缓存的性能问题此处不做深入讨论。
        本节将使用一个数组来作为缓存池,从而实现一个缓存实例的不可变类。

         从图6.3中不难看出,当使用CacheImmutale 类的valueOf()方法来生成对象时,系统是否重新生成新的对象,取决于图6.3中被灰色覆盖的数组内是否已经存在该对象。如果该数组中已经缓存了该类的对象,系统将不会重新生成对象

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

扩展 :

        例如Java提供的java.lang.Integer类,它就采用了与CacheImmutale类相同的处理策略,如果采用new构造器来创建Integer 对象,则每次返回全新的Integer对象;如果采用valueOf()方法来创建Integer对象,则会缓存该方法创建的对象。下面程序示范了Integer 类构造器和valueOf()方法存在的差异。

         运行上面程序,即可发现两次通过Integer.valueOf(6);方法生成的 Integer对象是同一个对象。但由于Integer 只缓存-128~127之间的Integer对象,因此两次通过Integer.valueOf(200);方法生成的Integer对象不是同一个对象。

散称知识点:

猜你喜欢

转载自blog.csdn.net/indeedes/article/details/120881452