6.4.5 final方法

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

Object类中的final方法

Java提供的Object类里就有一个final方法:getClass(),因为Java不希望任何类重写这个getClass方法,所以使用final把这个方法密封起来。
但对于该类提供的toStringequals方法,都允许子类重写,因此没有使用final修饰它们。

注意final修饰private方法的情况

private方法子类不可见无法重写

对于一个private方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写private方法

子类定义相同的private方法也不算重写

如果子类中定义一个与父类private方法有相同方法名相同形参列表相同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。

子类可以定义与父类同名的final修饰的private方法

因此,即使使用final修饰一个private访问权限的方法,依然可以在其子类中定义与该方法具有相同方法名相同形参列表相同返回值类型的方法。

final修饰的方法可以被重载

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

6.4.4 可执行 宏替换的 final变量

什么样的final变量可以当成直接量

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

  • 在定义该final变量时指定了初始值。
  • 该初始值可以在编译时就被确定下来。

编译器会将final直接量变量 直接 替换成对应的值

当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个final变量本质上就是一个”宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值

字符串直接量

Java会使用常量池来管理曾经用过的字符串直接量,例如执行String a="java";语句之后,常量池中就会缓存一个字符串"java";如果程序再次执行String b="java";系统将会让b直接指向常量池中的"java"字符串,因此a==b将会返回true

final变量当成宏变量处理的情况

  • 定义final变量时要直接赋值,赋值可以是一个简单的直接量,也可以是一个表达式
  • 如果被赋的表达式只是基本的算术表达式字符串连接运算,则Java编译器同样会将这种final变量当成”宏变量”处理。

注意:如果赋值表达式包含了普通变量,或者调用了方法则不会当成”宏变量”处理.

对于final实例变量而言,只有在定义该变量时指定初始值才会有”宏变量”的效果

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class FinalReplaceTest
{
public static void main(String[] args)
{
// 算术表达式,会被当成"宏变量"
final int a = 5 + 2;
// 算术表达式,会当成"宏变量"
final double b = 1.2 / 3;
// 字符串直接量的连接,并且是final修饰的 所以str会被当成"宏变量"
final String str1="HelloWorld";
final String str2 = "Hello" + "World";
// 是同一个对象,输出true
System.out.println(str1==str2);
// 字符串直接量和数值直接量的链接,而且是final修饰的所以变量book会被当成"宏变量"
final String book = "HelloWorld" + 99.0;
// 调用了方法,会生成新对象
final String book2 = "HelloWorld" + String.valueOf(99.0);
double num=99.0;
// 调用了变量num,不是直接量
final String book3="HelloWorld"+num;
// HelloWorld99.0
System.out.println(book3);
// 相等,是同一字符串对象,输出true
System.out.println(book == "HelloWorld99.0");
// 不相等,不是同一个字符串对象,输出
System.out.println(book2 == "HelloWorld99.0");
// 不相等,不是同一个字符串对象,输出
System.out.println(book3 == "HelloWorld99.0");
}
}

这个示例并不严谨,这本质上是String直接量
运行结果:

1
2
3
4
5
true
HelloWorld99.0
true
false
false

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

final修饰基本类型变量时 基本类型变量不可变

当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变,

final修饰引用类型变量时 引用变量不可指向其他对象

但对于引用类型变量而言,它保存的仅仅是一个引用, final只保证这个引用类型变量所引用的地址不会改变,即要一直引用同一个对象,但这个对象完全可以发生改变。

虽然引用变量不可指向其他对象 但对象的值可以改变

使用final修饰的引用类型变量不能被重新赋值,但可以改变引用类型变量所引用的对象的内容。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.*;

class Person
{
private int age;
public Person(){}
// 有参数的构造器
public Person(int age)
{
this.age = age;
}
// 省略age的setter和getter方法
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
public class FinalReferenceTest
{
public static void main(String[] args)
{
// final修饰数组变量,iArr是一个引用变量
final int[] iArr = {5, 6, 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 Person p = new Person(45);

// 改变Person对象的age实例变量,合法
p.setAge(23);
System.out.println(p.getAge());

// 下面语句对p重新赋值,非法
// p = null;
}
}

6.4.2 final局部变量

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

  • 使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。
    • 如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该final变量赋初始值,但只能一次,不能重复赋值;
    • 如果final修饰的局部变量在定义时已经指定默认值,则后面代码中不能再对该变量赋值。

不要在方法中对final形参赋值

因为形参在调用该方法时,系统根据传入的实参来对形参赋值,所以方法中不要再对final修饰的形参再次赋值。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FinalLocalVariableTest
{
public void test(final int a)
{
// 方法中不能再对final修饰的形参赋值,下面语句非法
// a = 5;
}
public static void main(String[] args)
{
// 定义final局部变量时指定默认值,则str变量无法重新赋值
final String str = "hello";
// 下面赋值语句非法
// str = "Java";

// 定义final局部变量时没有指定默认值,则d变量可被赋值一次
final double d;
// 第一次赋初始值,成功
d = 5.6;
// 对final变量重复赋值,下面语句非法
// d = 3.4;
}
}

6.4 final修饰符

成员变量是随类初始化对象初始化而初始化的。
类初始化时,系统会为该类的类变量分配内存,并分配默认值;
创建对象时,系统会为该对象的实例变量分配内存,并分配默认值。

也就是说,当执行静态初始化块时可以对类变量赋初始值;
当执行普通初始化块构造器时可对实例变量赋初始值。

因此,成员变量的初始值可以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值

final修饰的成员变量只能赋值一次

对于final修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,

必须显示初始化final成员变量

如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0\u0000falsenull,这些成员变量也就完全失去了存在的意义。

因此Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值

final修饰的成员变量的合法初始化位置

归纳起来, final修饰的类变量、实例变量能指定初始值的地方如下。

  1. 类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定,不可以在其他地方指定初始值.
  2. 实例变量:必须在非静态初始化块声明该实例变量时指定初始值在构造器中指定初始值,而且只能在三个地方的其中之一指定。不可以在其他地方指定初始值.

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class FinalVariableTest
{
// 定义成员变量时指定默认值,合法。
final int a = 6;
// 下面变量将在构造器或初始化块中分配初始值
final String str;
final int c;
final static double d;
// 既没有指定默认值,又没有在初始化块、构造器中指定初始值,
// 下面定义的ch实例变量是不合法的。
// final char ch;
// 初始化块,可对没有指定默认值的实例变量指定初始值
{
//在初始化块中为实例变量指定初始值,合法
str = "Hello";
// 定义a实例变量时已经指定了默认值,
// 不能为a重新赋值,因此下面赋值语句非法
// a = 9;
}
// 静态初始化块,可对没有指定默认值的类变量指定初始值
static
{
// 在静态初始化块中为类变量指定初始值,合法
d = 5.6;
}
// 构造器,可对既没有指定默认值、有没有在初始化块中
// 指定初始值的实例变量指定初始值
public FinalVariableTest()
{
// 如果在初始化块中已经对str指定了初始化值,
// 构造器中不能对final变量重新赋值,下面赋值语句非法
// str = "java";
c = 5;
}
public void changeFinal()
{
// 普通方法不能为final修饰的成员变量赋值
// d = 1.2;
// 不能在普通方法中为final成员变量指定初始值
// ch = 'a';
}
public static void main(String[] args)
{
FinalVariableTest ft = new FinalVariableTest();
System.out.println(ft.a);
System.out.println(ft.c);
System.out.println(ft.d);
}
}

6.3.2 单例(Singleton)类

单例类

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

单例模式详解

  1. 为了避免其他类自由创建该类的实例,应该把该类的构造器使用private修饰,从而把该类的所有构造器隐藏起来。
  2. 一旦把该类的构造器隐藏起来,就需要提供一个public方法作为该类的访问点,用于创建该类的对象,且该方法必须使用static修饰(因为调用该方法之前还不存在对象,因此只能是类来调用该方法)。
  3. 除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过对象,也就无法保证只创建一个对象。为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,故该成员变量必须使用static修饰。

6.3 类成员

static关键字修饰的成员就是类成员,前面已经介绍的类成员类变量类方法类初始化块三个成员,

static不能修饰构造器

static关键字不能修饰构造器。 static修饰的类成员属于整个类,不属于单个实例。

6.3.1 理解类成员

java类中的五种成员

在Java类里只能包含:

  • 成员变量、
  • 方法、
  • 构造器、
  • 初始化块、
  • 内部类(包括接口、枚举)

这5种成员,

static可以修饰哪些类成员

static可以修饰成员变量方法初始化块内部类(包括接口、枚举)这四种成员,以static修饰的成员就是类成员。类成员属于整个类,而不属于单个对象。

类变量详解

类变量属于整个类,当系统第一次准备使用该类时,系统会为该类变量分配内存空间,类变量开始生效,直到该类被卸载,该类的类变量所占有的内存才被系统的垃圾回收机制回收。类变量生存范围几乎等同于该类的生存范围。当类初始化完成后,类变量也被初始化完成。
类变量既可通过来访问,也可通过类的对象来访问当通过对象来访问类变量时,系统会在底层转换为通过该类来访问类变量
从程序运行表面来看,可以看到同一类的所有实例的共享同一个类变量

类方法详解

类方法也是类成员的一种,类方法也是属于类的,通常直接使用类作为调用者来调用类方法,但也可以使用对象来调用类方法。与类变量类似,即使使用对象来调用类方法,其效果也与采用类来调用类方法完全一样。

对象访问类成员都是通过类来访问

当使用实例来访问类成员时,实际上依然是委托给该类来访问类成员,因此即使某个引用变量的值为null它也可以访问它所属类的类成员

静态初始化块

静态初始化块也是类成员的一种,静态初始化块用于执行类初始化动作,在类的初始化阶段,系统会调用该类的静态初始化块来对类进行初始化。一旦该类初始化结束后,静态初始化块将永远不会获得执行的机会

类成员不能访问实例成员

static关键字而言,有一条非常重要的规则:类成员不能访问实例成员

为什么类成员不能访问实例成员

因为类成员是属于类的,类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。

6.2.2 ==和equals方法

Java程序中测试两个变量是否相等有两种方式:

  • 一种是利用==运算符,
  • 另一种是利用equals()方法。

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

常量池

常量池(constant pool)专门用于管理在编译时被确定被保存在已编译的.class文件中的一些数据。它包括了关于方法接口中的常量,还包括字符串常量

常量池管理字符串的情形

Java程序直接使用形如"hello"的字符串直接量时,JVM将会使用常量池来管理这些字符串;当使用new String("hello")时,JVM会先使用常量池来管理"hello"直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。换句话说, new String("hello")一共产生了两个字符串对象
JVM常量池保证相同的字符串直接量只有一个,不会产生多个副本。

怎样的字符串可以在编译器确定

  1. 一条语句中只有字符串直接量的连接可以在编译时确定下来
  2. 字符串引用变量连接无法在编译时确定
  3. 字符串引用变量字符串直接量连接 也无法在编译时确定

字符串通过==运行符比较的情况

  1. 对应在编译期就可以确定下来的字符串直接量的引用变量,这些引用变量所引用的都是常量池中的同一个字符串对象。所以通过"=="运算符比较相等.
  2. 使用new String()创建的字符串对象是运行时创建出来的,该字符串对象被保存在运行时内存区,也就是保持在堆内存内,不会放入常量池中。所以两个通过new创建的字符串通过"=="比较将返回false

equals方法

equals方法是Object类提供的一个实例方法,因此所有引用变量都可调用该方法来判断是否与其他引用变量相等。

Object提供的equals方法只比较引用变量的地址是否一致

Object默认提供的equals()只是比较引用变量的地址是否一致,也就是说Object类的equals()方法比较的结果与”==“运算符比较的结果完全相同。

提供自定义的相等标准

如果希望采用自定义的相等标准,则可采用重写equals方法来实现。重写equals方法就是提供自定义的相等标准,你甚至可以让Person对象和Dog对象相等.

String重写的equals方法

String已经重写了Objectequals方法, Stringequals方法判断两个字符串相等的标准是:只要两个字符串所包含的字符序列相同,通过equals比较将返回true,否则将返回false

重写equals方法的条件

通常而言,正确地重写equals方法应该满足下列条件。

  1. 自反性:对任意x, x.equals(x)一定返回true
  2. 对称性:对任意xy,如果y.equals(x)返回true,则x.equals(y)也返回true
  3. 传递性:对任意x,y,z,如果x.equals(y)返回true, y.equals(z)返回true,则x.equals(z)一定返回true
  4. 一致性:对任意xy,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么一直是true,要么一直是false
  5. 对任何不是nullx,x.equals(null)一定返回false

6.2 处理对象

Java对象都是Object类的实例,都可直接调用该类中定义的方法,这些方法提供了处理Java对象的通用方法。

6.2.1 打印对象和toString方法

所有Java对象都有toString方法

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

对象和字符串的连接运算

不仅如此,所有的java对象都可以和字符串进行连接运算,Java对象和字符串进行连接运算时,系统自动调用Java对象toString方法的返回值和字符串进行连接运算
toString方法是一个非常特殊的方法,它是一个”自我描述”方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统将会输出该对象的”自我描述”信息,用以告诉外界该对象具有的状态信息。

Object提供的toString方法

Object类提供的toString方法总是返回该对象实现类的”类名+@+hashCode"值,这个返回值并不能真正实现”自我描述”的功能,因此如果用户需要自定义类能实现”自我描述”的功能,就必须重写Object类的toString方法。

6.1 Java8增强的包装类

8种基本数据类型不支持面向对象

Jaa是面向对象的编程语言,但它也包含了8种基本数据类型,这8种基本数据类型不支持面向对象的编程机制,基本数据类型的数据也不具备”对象”的特性:没有成员变量、方法可以被调用。

所有的引用变量都可以当成Object类的引用变量使用

所有引用类型的变量都继承了Object类,都可当成Object类型变量使用。但基本数据类型的变量就不可以

包装类的引入

为了解决8种基本数据类型的变量不能当成Object类型变量使用的问题,Java提供了包装类(Wrapper Class)的概念,为8种基本数据类型分别定义了相应的引用类型,并称之为基本数据类型的包装类

基本数据类型和包装类的对应关系

这里有一张图片

基本数据类型和包装类的命名规则

除**intchar有点例外之外,其他的基本数据类型对应的包装类都是将基本数据类型首字母大写即可**

JDK1.5之前 基本数据类型变成包装类对象

JDK1.5以前,把基本数据类型变量变成包装类实例需要通过对应包装类的valueOf()静态方法来实现。

JDK1.5之前 包装类对象转基本数据类型

JDK1.5以前,如果希望获得包装类对象中包装的基本类型变量,则可以使用包装类提供的xxxValue()实例方法。
JDK1.5之后由于这种用法已经过时。
这里有一张图片

自动装箱 自动拆箱

为了消除基本类型变量和包装类对象之间的烦琐转换过程,JDK1.5提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能

什么是自动装箱

所谓自动装箱,就是可以把一个基本类型变量直接赋给对应的包装类变量,或者赋给Object变量,因为Object是所有类的父类,所以子类对象可以直接赋给父类变量;

什么是自动拆箱

自动拆箱则与之相反,允许直接把包装类对象直接赋给一个对应的基本类型变量

自动装箱时注意类型匹配

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

字符串类型的值转为基本类型类型的值

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

  1. 利用包装类提供的parseXxx(String s)静态方法(除Character之外的所有包装类都提供了该方法)
  2. 利用包装类提供的valueOf(String s)静态方法。

基本类型变量转为字符串

  1. String类提供了多个重载valueOf()方法,用于将基本类型变量转换成字符串.
  2. 将基本类型变量和空字符串("")进行连接运算,则系统自动把基本类型变量转换成字符串

这里有一张图片

包装类对象可以直接和基本类型直接比较

包装类的实例可以与数值类型的值进行比较,这种比较是直接取出包装类实例所包装的数值来进行比较的。

比较两个包装类对象的值

两个包装类的实例进行比较的情况就比较复杂,因为包装类的实例实际上是引用类型,只有两个包装类引用指向同一个对象时才会返回true

如果要比较两个包装类对象的值,需要先转成基本类型再比较。不能直接使用==运算符直接比较.(因为对于基本类型”==“比较的是值,对于引用类型,==比较的是两个引用变量是否指向同一个对象。)

系统会自动缓存负128到127的Integer对象

系统自动缓存负128正127之间的Integer对象,当自动装箱的int变量的值在负128正127之间时,将直接返回缓存好的Integer对象,不会创建新的Integer对象.

Integer缓存引起的问题

对于两个在负128正127之间int类型的变量ab,如果ab相等,则ab自动装箱后得到的是同一个Integer对象.
由于是同一个Integer对象,所以这两个对象的引用变量通过==运算符比较将得到true

1
2
3
4
5
6
7
8
9
// 通过自动装箱,允许把基本类型值赋值给包装类实例
Integer ina=2;
Integer inb =2;
//-128<=2<=127 ina和inb是同一个对象,所以输出true
System.out.println("两个2自动装箱后是否相等:"+(ina==inb);
Integer biga =128;
Integer bigb =128;
//-128<=128<=127 不成立 ina和inb不是同一个对象,所以输出false
System.out. println("两个128自动装箱后是否相等:"+(biga==bigb));

java版本对包装类的增强

Java7后通过包装类的compare来比较对于基本类型的大小

Java7增强了包装类的功能,Java7所有的包装类都提供了一个静态的compare(基本类型 变量1, 基本类型 变量2)方法,这样开发者就可以通过包装类提供的compare方法来比较两个基本类型值的大小

包括比较两个boolean类型值,两个boolean类型值进行比较时,true>false

Java7为Character包装类增加了大量工具方法

不仅如此,Java7还为Character包装类增加了大量的工具方法来对一个字符进行判断。关于Character中可用的方法请参考CharacterAPI文档。

java8增加无符号运算

Java8再次增强了这些包装类的功能,其中一个重要的增强就是支持无符号算术运算Java8为整型包装类增加了支持无符号运算的方法。Java8IntegerLong增加了如下方法。

无符号运算方法 描述
static String toUnsignedString(int或者long i) 该方法将指定intlong型整数转换为无符号整数对应的字符串。
static String toUnsignedString(int或者long i, int radix) 该方法将指定intlong型整数转换为指定进制的无符号整数对应的字符串
static xxx parseUnsignedXxx(String s) 该方法将指定字符串解析成无符号整数
static xxx parseUnsignedXxx(String s, int radix) 该方法将指定字符串按指定进制解析成无符号整数
static long divideUnsigned(long dividend, long divisor) 该方法将x、y两个整数转换为无符号整数后,计算它们相除的商。
static long remainderUnsigned(long dividend, long divisor) 该方法将x、y两个整数转换为无符号整数后计算它们相除的余数.

当调用类为Integer时,xxx代表int;当调用类是Long时,xxx代表long

Java8还为ByteShort增加了toUnsignedInttoUnsignedLong两个方法,这两个方法用于将指定byteshort类型的变量或值转换成无符号的intlong值。

无符号整数的特点

无符号整数最大的特点是最高位不再被当成符号位,因此无符号整数不支持负数,其最小值为0