VectorLu

Java 从 Hello World 到面向对象

Java 的基础知识和细节纪录。如有谬误,请不吝赐教。

第一个 Java 程序

编写/编译/执行

1
2
3
4
5
6
7
public class HelloWorld
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}

将上面的文本保存为 HelloWorld.java,接下来编译该 Java 源文件来生成字节码。

1
javac -d destdir srcFile
  • -d destdirjavac 命令的选项,用以指定编译生成的字节码文件的存放路径。如果省略,则目标文件存放在当前路径下。
  • srcFile 是 Java 源文件所在的位置。

这样就会生成字节码文件 HelloWorld.class

运行

1
java HelloWorld

注意是类名,没有 .java 后缀,也没有 .class 后缀。

程序设计思想

面向过程

1
吃(猪八戒,西瓜);

面向对象

1
猪八戒.吃(西瓜)

面向对象的语句更接近自然语言的语法:主语、谓语、宾语一目了然,十分直观。

基于对象

也使用了对象的概念,但和面向对象不同的是,无法利用现有的对象模版产生新的对象类型,继而产生新的对象。

判断一门语言是否是面向对象的,通常使用“继承”和“多态”加以判断。“面向对象”和“基于对象”都实现了“封装”的概念,但是“基于对象“没有实现这些。

数据类型和运算符

分隔符

1
; {} [] () . 空格符

标识符规则

标识符可以由字母、数字、_ 和美元符 $ 组成,其中数字不能打头。

Java 关键字

1 2 3 4 5
abstract continue for new switch
assert default if package synchronized
boolean do goto private this
break double implements protected throw
byte else import public throws
case enum instanceof return transient
catch extends int short try
char final interface static void
class finally long strictfp volatile
const float native super while

另外,Java 还提供了三个特殊的直接量 (literal): true, false, null,Java 语言的标识符也不能使用这三个特殊的直接量。

数据类型

Java 是强类型语言:

  1. 所有的变量必须先声明,后使用。
  2. 指定类型的变量只能接受类型与之匹配的值。这意味着每个变量和每个表达式都有一个在编译时就确定的类型。

强类型语言可以在编译时进行更加严格的语法检查,从而减少错误。

Java 语言支持两种类型:

  1. 基本类型 (Primitive Type):
    1. boolean 类型:值只能是 truefalse不能用 0 或者非 0 代表,其他基本数据类型的值也不能转换成 boolean 类型。
    2. 数值类型:
      1. 整数类型:byte 1 个字节、short 2 个字节、int 4 个字节、long 8 个字节。
      2. 字符类型:char 2 个字节,支持 16 位的 Unicode 字符
      3. 浮点类型:float 4 个字节、double 8 个字节
  2. 引用类型 (Reference Type):类、接口和数组类型,还有一种特殊的 null 类型。对象包括实例和数组两种,实际上,引用类型变量就是一个指针。null 只能转换成引用类型,不能转换成基本类型,因此不要把一个 null 值赋给基本数据类型的变量。
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
// 下面代码是正确的,直接把一个较小的整数值赋给一个 byte 或 short 变量
byte a = 56;
// 下面代码是错误的,如果使用一个巨大的整数值(超出了 int 类型的表数范围)
// Java 不会自动把这个整数值当成 long 类型来处理
// 应该在该整数值后加上 L 作为后缀
long bigValue = 999999999999999999;
// 下面的代码是正确的,使用 L 后缀,强制使用 long 类型
long bigValue2 = 99999999999999999L;
// 为了让数值更清晰,Java7 之后,可以在数值中使用下划线
// 无论是整型数值,还是浮点型数值
int binVal = 0B1000_0000_0000_0000_0000_0000_0000_0011;
double pi = 3.14_15_92;
// 定义 b1 的值为 true
boolean b1 = true;
boolean b2 = false;
// 使用一个 boolean 类型的值和字符串进行连接运算
// boolean 类型的值会自动转换为字符串
String str = true + " ";
// print true
System.out.println(str);
// print true
System.out.println(true);

基本类型的类型转换

自动类型转换

表数范围小的可以像表数范围大的进行自动类型转换。

byte < short < int < long < float < double

char < int < long < float < double

这是两条线,主要在于 byte 类型不能自动转换为 char 类型。

当任何基本类型的值和字符串值进行连接运算时,基本类型的值将自动类型转换为字符串类型。因此,如果希望把基本类型的值转换为对应的字符串时,可以把基本类型的值和一个空字符串进行连接。+ 不仅可以作为加法运算符,还可以作为字符串连接运算符使用。

1
2
3
4
5
// 下面语句输出7Hello!
System.out.println(3 + 4 + "Hello!");
// 下面语句输出Hello!34,因为Hello! + 3会把3当成字符串处理,
// 而后再把4当成字符串处理
System.out.println("Hello!" + 3 + 4);

强制类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class NarrowConversion
{
public static void main(String[] args)
{
int iValue = 233;
// 强制把一个int类型的值转换为byte类型的值
byte bValue = (byte)iValue;
// 将输出-23
System.out.println(bValue);
double dValue = 3.98;
// 强制把一个double类型的值转换为int
int tol = (int)dValue;
// 将输出3
System.out.println(tol);
String a = "45";
// 调用基本类型对应的包装类的方法
// 使用 Integer 的方法将一个字符串转换成 int 类型
int iValue = Integer.parseInt(a);
}
}

表达式类型的自动提升

byte < short < int < long < float < double

char < int < long < float < double

与表数范围最大的类型保持一致。

流程控制

switch

switch 语句后面的控制表达式的数据类型是 byteshortcharint 4 种整数类型,枚举类型和 java.lang.String 类型(从 Java7 才允许),不能是 boolean 类型。

其他与 C 语言完全类似。

数组

Java 为工具类的命名习惯是添加一个字母 s,比如操作数组的工具类是 Arrays,操作集合的工具类是 Collections

声明

数组在 Java 里是一种类型(引用类型)

1
2
3
4
5
6
// 推荐声明方式:typeName arrayName
int[] intArray;
// 不推荐,这是 C 语言中的定义方式
// 越来越多的语言不支持这种定义方式,比如 C#
int intArray[];

初始化

只是声明数组还不能使用它,用 new 运算符创建数组

1
int[] intArray = new int[100];

创建一个数组时:

  • byte, short, intlong 这样的整数类型都被初始化为 0
  • boolean 数组的元素会初始化为 false
  • 对象数组的元素则初始化为 null,表示这些元素还未存放任何对象。

获得数组中元素的个数:

1
2
for (int i = 0; i < intArray.length; i++)
System.out.println(a[i]);

一旦创建了数组,就不能再改变其大小。

初始化的几种方法:

1
2
3
4
5
// 创建数组对象并赋初值,不需要调用 new
int[] smallPrimes = {2, 3, 5, 7, 11, 13};
// 初始化匿名数组可以在不创建新变量,重新初始化一个数组
smallPrimes = new int[] {17, 19, 23, 29, 31};

在 Java 中,允许数组长度为 0,在编写一个返回数组的方法时,这种语法形式就格外有用,可以创建一个长度为 0 的数组,new elementType[0]

for each

用来依次处理数组中的每个元素(其他类型的元素集合亦可),不必指定下标值。

1
2
for (variable : collection)
{statement block}

collection 是一个数组或者是一个实现了 Iterable 接口的类对象(例如 ArrayList)。

注意到循环变量是一个临时变量,系统会把数组元素依次赋给这个临时变量,但是这个临时变量不是数组元素,如果希望改变数组的元素的值,则不能使用 for each 这种方法。如果是想打印数组,可以利用 Arrays 类的 toString() 方法(注意使用这个方法要导入包 import java.util.Arrays):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Arrays;
public class ForEachTest
{
public static void main(String[] args)
{
String[] passwords = {"天王盖地虎",
"宝塔镇河妖", "举头望明月", "低头思故乡"};
for (String password : passwords)
{
System.out.println(password);
}
System.out.println("-----我是 乖巧 端庄 严肃 正直 的分隔线-----");
System.out.println(Arrays.toString(passwords));
}
}

数组拷贝

在 Java 中,允许将一个数组用 = 拷贝给另一个数组,实际上,是这两个变量引用同一个数组:

1
2
3
4
int[] smallPrimes = {2, 3, 5, 7, 11, 13};
int[] luckyNum = smallPrimes;
luckyNum[5] = 12; // now smallPrimes[5] = 12

如果想要把一个数组的所有值都拷贝到一个新数组中,就要用 Arrays 类的 copyOf() 方法。

在上面的代码中加入:

1
String[] backup = Arrays.copyOf(passwords, passwords.length);

第二个参数是数组的长度,可以用这种方法初始化一个不同长度的数组。

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
import java.util.Arrays;
public class ForEachTest
{
public static void main(String[] args)
{
String[] passwords = {"天王盖地虎",
"宝塔镇河妖", "举头望明月", "低头思故乡"};
String[] shorterBackup = Arrays.copyOf(passwords, passwords.length - 2);
String[] longerBackup = Arrays.copyOf(passwords, passwords.length + 2);
for (String password : passwords)
{
System.out.println(password);
}
System.out.println("----- 我是 乖巧 端庄 严肃 正直 的分隔线1 -----");
System.out.println(Arrays.toString(passwords));
System.out.println("----- 我是 乖巧 端庄 严肃 正直 的分隔线2 -----");
System.out.println(Arrays.toString(shorterBackup));
System.out.println("----- 我是 乖巧 端庄 严肃 正直 的分隔线3 -----");
System.out.println(Arrays.toString(longerBackup));
}
}

运行效果如下:

1
2
3
4
5
6
7
8
9
10
天王盖地虎
宝塔镇河妖
举头望明月
低头思故乡
----- 我是 乖巧 端庄 严肃 正直 的分隔线1 -----
[天王盖地虎, 宝塔镇河妖, 举头望明月, 低头思故乡]
----- 我是 乖巧 端庄 严肃 正直 的分隔线2 -----
[天王盖地虎, 宝塔镇河妖]
----- 我是 乖巧 端庄 严肃 正直 的分隔线3 -----
[天王盖地虎, 宝塔镇河妖, 举头望明月, 低头思故乡, null, null]

面向对象

staticthis

在静态方法中直接访问实例方法时会引发错误。如果真的需要在静态方法中使用普通的实例方法,则需要创建一个实例对象来调用实例方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StaticAccessNonStatic
{
public void info()
{
System.out.println("简单的info方法");
}
public static void main(String[] args)
{
// 因为main()方法是静态方法,而info()是非静态方法,
// 调用main()方法的是该类本身,而不是该类的实例,
// 因此省略的this无法指向有效的对象
info();
}
}

报错:

1
2
3
4
StaticAccessNonStatic.java:24: error: non-static method info() cannot be referenced from a static context
info();
^
1 error

大部分时候,普通方法访问其他方法、成员变量时无须使用 this 前缀,但如果方法里有个局部变量与成员变量同名,但程序有需要在该方法里访问这个被覆盖的成员变量,则必须使用 this 前缀。

方法

方法的参数传递机制

Java 里方法的参数传递方式只有一种——值传递(将实际参数值的副本传入方法内,而参数本身不会收到任何影响)。

引用变量传递的是一个对象的地址值。

形参个数可变的方法

在定义方法时,在最后一个形参的类型后增加三点 ...,则表明该形参可以接受多个参数值,多个参数值被当成数组传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Varargs
{
// 定义了形参个数可变的方法
public static void test(int a , String... books)
{
// books被当成数组处理
for (String tmp : books)
{
System.out.println(tmp);
}
// 输出整数变量a的值
System.out.println(a);
}
public static void main(String[] args)
{
// 调用test方法
test(5 , "傲慢与偏见" , "双城记");
}
}

也可以用数组形参来定义方法。

1
public static void test(int a, String[] books);

这两种方法都包含了一个名为 books 的形参,在方法体内都可以把 books 当成数组处理,但是区别是调用两个方法时存在差别,对于以可变形参的形式定义的方法,调用方法更加简洁:

1
test(5, "傲慢与偏见", "双城记");

传给 books 参数的实参数值无须是一个数组,但如果采用数组形参来声明方法,调用时则必须传入一个数组。

1
test(5, new String[]{"傲慢与偏见", "双城记"});

明显第一种形式更加简洁,但是,数组形式的形参可以位于形参列表的任意位置,但个数可变的形参只能位于形参列表的最后。也就是说,一个方法中最多只能有一个长度可变的形参。

封装

对一个类或对象实现良好的封装,可以实现一下目的:

  1. 隐藏类的视线细节。
  2. 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问。
  3. 可进行数据检查,从而有利于保证对象信息的完整性。
  4. 便于修改,提高代码的可维护性。

为了实现良好的封装,需要从两个方面考虑:

  1. 把对象的成员变量和实现细节隐藏起来,不允许外部直接访问。
  2. 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。

特别注意:不要编写返回引用可变对象的 get 方法。这样会破坏封装性!

1
2
3
4
5
6
7
8
9
10
11
12
13
class Employee {
private Date hireDay;
...
public Date getHireDay() {
return hireDay;
}
}
...
Employee Harry = ...;
Date d = Harry.getHireDay();
double tenYearsInMilliSeconds = 10*365.25*24*60*60*1000;
d.setTime(d.getTime() - (long) tenYearsInMilliSeconds);

dHarry.hireDay 引用同一个对象。d.setTime(...) 就自动地改变了这个雇员对象的私有状态。

使用访问控制符

3 个访问控制符:private, protectedpublic

4 个访问控制级别(由小到大):private, default, protected, public。详细介绍:

  1. private当前类访问权限,如果类里的一个成员(包括成员变量、方法和构造器等)使用 private 访问控制符来修饰,则这个成员只能在当前类的内部被访问。用于修饰成员变量非常合适,可以将其隐藏在该类的内部。
  2. default包访问权限,类里的一个成员(包括成员变量、方法和构造器等不使用任何访问权限控制符修饰。default 访问控制的成员或外部类可以被相同包下的其他类访问。
  3. protected子类访问权限,该成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。通常情况下,如果使用 protected 来修饰一个方法,通常是希望其子类来重写这个方法。
  4. public:即没有访问限制。

需要注意的是:访问控制符用于控制一个类的成员是否可以被其他类访问,对于局部变量而言,其作用域就是它所在的方法或代码块,不可能被其他类访问,因此不能使用访问控制符来修饰。

package

Java 引入了包 (package) 机制,提供了类的多层命名空间,用于解决类的命名冲突、类文件管理等问题。将一组功能相关的类放在同一个 package 下,从而组成逻辑上的类库单元。如果希望把一个类放在指定的包结构下,应该在 Java 源程序的第一个非注释行放置如下格式的代码:

1
package packageName;

一旦在 Java 源文件中使用了这个 package 语句,就以为着该源文件里定义的所有类都属于这个包。如果其他人需要使用该包下的类,也应该使用包名加类名的组合。

package 语句必须作为源文件的第一条非注释性语句,一个源文件最多只能包含一条 package 语句,该源文件中可以定义多个类,则这些类将全部位于该包下。

import 关键字

import 可以向某个 Java 文件中导入指定包层次下某个类或全部类,import 语句应该出现在 package 语句之后、类定义之前。

如果导入 vector.sub.Hello 类,应该使用如下代码:

1
import vector.sub.Hello;

使用 import 语句导入指定包下全部类:

1
import package.subpackage...*

其中 * 星号只能代表类,不能代表包,因此使用 import vector.* 语句时,它表明导入 vector 包下的所有类,但是如果 vector 包中有其他子包,这些子包中的类不会被导入,如需导入子包中的所有类,需要加上 import vector.sub.* 这样的语句。

Java 默认所有源文件导入 java.lang 包下的所有类,因此前面在 Java 程序中使用 String, System 类时都无须使用 import 语句来导入这些类。

某些情况下只能在源文件中使用类全名。例如,需要在程序中使用 java.sql 包下的类,也需要使用 java.util 包下的类,则可以使用如下 import 语句:

1
2
import java.util.*;
import java.sql.*;

如果接下来在程序中使用 Date 类,则会引起编译错误,因为上述两个包中都有这个类,而系统会不知道究竟要用哪个类,为了使引用更加明确,使用该类的全名:

1
java.sql.Date d = new java.sql.Date();

静态导入

导入指定类的单个静态成员变量、方法:

1
import static package.subpackage...ClassName.fieldName|methodName;

导入指定类的全部静态成员变量、方法:

1
import static package.subpackage...ClassName.*;

import 可以省略写包名;而使用 import static 则可以连类名都省略。

深入构造器

使用 this 调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。使用 this 调用重载的构造器时,系统会根据 this 后括号里的实参来调用形参列表与之对应的构造器。

继承

Java 只能有一个直接父类,摒弃了 C++ 中难以理解的多继承特征。

重写父类的方法

子类包含与父类同名方法的现象被称为方法重写,也被称为方法覆盖。遵循“两同两小一大”规则,覆盖方法和被覆盖方法要么都是静态方法,要么都是实例方法。

super 限定

如果需要在子类方法中调用父类中被覆盖的方法,则可以使用 super 作为调用者来调用父类中被覆盖的方法。

当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存,即使子类定义了与父类同名的实例变量。也就是说,当系统创建一个 Java 对象,如果该 Java 类有两个父类(一个直接父类 A,一个间接父类 B),假设 A 类中定义了 2 个实例变量,B 类中定义了 3 个实例变量,当前类中定义了 2 个实例变量,那么这个 Java 对象会保存 2+3+2 个实例变量。

由上,个人认为如无必要,尽量使子类应用父类的成员变量,避免内存的浪费。

调用父类构造器

子类不会获得父类的构造器,但子类构造器可以调用父类构造器的初始化代码。

在一个构造器中调用另一个重载的构造器使用 this 调用来完成,在子类构造器中调用父类构造器使用 super 调用来完成。使用 super 调用父类构造器也必须出现在子类构造器执行体的第一行,所以 thissuper 调用不会同时出现。

多态

仅针对方法而言。
编译类型为父类,运行类型为子类的对象,调用方法时,调用的是子类中重写父类的方法。这就会出现类型相同,而调用同一方法出现不同的行为特征,即多态。实例变量不具备多态性,故依然使用父类的实例变量。如下示例:

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
53
54
class BaseClass
{
public int book = 6;
public void base()
{
System.out.println("父类的普通方法");
}
public void test()
{
System.out.println("父类的被覆盖的方法");
}
}
public class SubClass extends BaseClass
{
//重新定义一个book实例变量隐藏父类的book实例变量
public String book = "轻量级Java EE企业应用实战";
public void test()
{
System.out.println("子类的覆盖父类的方法");
}
public void sub()
{
System.out.println("子类的普通方法");
}
public static void main(String[] args)
{
// 下面编译时类型和运行时类型完全一样,因此不存在多态
BaseClass bc = new BaseClass();
// 输出 6
System.out.println(bc.book);
// 下面两次调用将执行BaseClass的方法
bc.base();
bc.test();
// 下面编译时类型和运行时类型完全一样,因此不存在多态
SubClass sc = new SubClass();
// 输出"轻量级Java EE企业应用实战"
System.out.println(sc.book);
// 下面调用将执行从父类继承到的base()方法
sc.base();
// 下面调用将执行从当前类的test()方法
sc.test();
// 下面编译时类型和运行时类型不一样,多态发生
BaseClass ploymophicBc = new SubClass();
// 输出6 —— 表明访问的是父类对象的实例变量
System.out.println(ploymophicBc.book);
// 下面调用将执行从父类继承到的base()方法
ploymophicBc.base();
// 下面调用将执行从当前类的test()方法
ploymophicBc.test();
// 因为ploymophicBc的编译类型是BaseClass,
// BaseClass类没有提供sub方法,所以下面代码编译时会出现错误。
// ploymophicBc.sub();
}
}

继承与组合

使用父类的注意要点

设计父类

为了保证父类有良好的封装性,不被子类随意更改,应遵循以下规则:

尽量隐藏父类的内部数据

尽量把父类的所有成员都设计成 private 类型,不要让子类直接访问父类的成员变量。

不要让子类可以随意访问父类的方法

父类中那些仅为辅助其他的工具方法,应该使用 private 访问控制符修饰。但又不希望子类重写该方法,可以使用 final 修饰符。如果希望父类的某个方法被子类重写,但是不希望被其他类自由访问,则可以使用 protected 来修饰该方法。

尽量不要在父类构造器中调用要被子类重写的方法
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
class Base
{
public Base()
{
test();
}
public void test() // 1 号test()方法
{
System.out.println("将被子类重写的方法");
}
}
public class Sub extends Base
{
private String name;
public void test() // 2 号test()方法
{
System.out.println("子类重写父类的方法,"
+ "其name字符串长度" + name.length());
}
public static void main(String[] args)
{
// 下面代码会引发空指针异常
Sub s = new Sub();
}
}

当系统试图创建 sub 对象时,同样会先执行其父类构造器,如果父类构造器调用了被其子类重写的方法,则变成调用被子类重写之后的方法。如上例,Base 构造器调用了
2 号 test() 方法,但是此时 Sub 对象的 name 实例变量是 null,因此将引发空指针异常。

构建子类的必要条件

  1. 子类需要额外添加属性,而不仅仅是属性值的改变。
  2. 子类需要添加自己独有的行为方式(包括添加新的方法或重写父类的方法)。

如果只是出于子类复用的目的,并不一定需要使用继承,完全可以使用组合来实现。

利用组合实现复用

初始化块

初始化块是构造器的补充,初始化块总是在构造器执行之前执行。它是一段固定执行的代码,它不能接收任何参数。因此初始化块对同一个类的所有对象所进行的初始化处理完全相同。

如果有一段初始化处理代码对所有对象完全相同,且无须接收任何参数,就可以把这段初始化处理代码提取到初始化块中。

其实初始化块是一个假象,使用 javac 命令编译 Java 类后,该 Java 类中的初始化块会被还原到每个构造器中,且位于构造器中所有代码的前面。

静态初始化块

使用了 static 修饰符,在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。

静态初始化块也属于类的静态成员——同样需要遵循静态成员不能访问非静态成员的规则,因此静态初始化块也不能访问非静态成员——实例变量和实例方法。

包装类

在 JDK 1.5 之后,提供了自动装箱和自动拆箱功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}
}
}

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

  1. parseXxx(String s)
  2. Xxx(String s)
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
public class Primitive2String
{
public static void main(String[] args)
{
String intStr = "123";
// 把一个特定字符串转换成int变量
int it1 = Integer.parseInt(intStr);
int it2 = new Integer(intStr);
System.out.println(it2);
String floatStr = "4.56";
// 把一个特定字符串转换成float变量
float ft1 = Float.parseFloat(floatStr);
float ft2 = new Float(floatStr);
System.out.println(ft2);
// 把一个float变量转换成String变量
String ftStr = String.valueOf(2.345f);
System.out.println(ftStr);
// 把一个double变量转换成String变量
String dbStr = String.valueOf(3.344);
System.out.println(dbStr);
// 把一个boolean变量转换成String变量
String boolStr = String.valueOf(true);
System.out.println(boolStr.toUpperCase());
}
}

如果希望把基本类型变量转换成字符串,有一种很简单的方式:

1
String intStr = 5 + "";

虽然包装类型的变量是引用数据类型,但包装类的实例可以与数值类型的值进行比较。

1
2
Integer wrapInt = new Integer(6);
System.out.println("6 的包装类实例是否大于 5.0“ + (a > 5.0));

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

1
2
// 输出 false
System.out.println("比较两个包装类的实例是否相等:" + (new Integer(2) == new Integer(2)));

自动装箱的特别情形:

1
2
3
4
5
6
7
// 通过自动装箱,允许把基本类型值赋值给包装类实例
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 系统中 java.lang.Integer 类的源代码,如下所示:

1
2
3
4
5
6
// 定义一个长度为 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 实例时,实际上是直接指向对应的数组元素,因此 -128 ~ 127 之间的同一个整数自动装箱成 Integer 实例时,永远都是引用 cache 数组的同一个数组元素,所以它们全部相等;但每次把一个不在 -128~127 范围内的整数自动装箱成 Integer 实例时,系统总是重新创建一个 Integer 实例,所以出现程序中的运行结果。

缓存是一种优秀的设计模式,把一些创建成本大、需要频繁使用的对象缓存起来,从而提高程序的运行性能。

处理对象

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

Object 类提供的 toString() 方法总是返回该对象实现类的 “类名+@+hashCode” 值,如果用户需要自定义类能实现“自我描述”的功能,就必须重写 Object 类的 toString() 方法。

==equals()

使用 ==

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class EqualTest
{
public static void main(String[] args)
{
int it = 65;
float fl = 65.0f;
// 将输出true
System.out.println("65和65.0f是否相等?" + (it == fl));
char ch = 'A';
// 将输出true
System.out.println("65和'A'是否相等?" + (it == ch));
String str1 = new String("hello");
String str2 = new String("hello");
// 将输出false
System.out.println("str1和str2是否相等?"
+ (str1 == str2));
// 将输出true
System.out.println("str1是否equals str2?"
+ (str1.equals(str2)));
// 由于java.lang.String与EqualTest类没有继承关系,
// 所以下面语句导致编译错误
// System.out.println("hello" == new EqualTest());
}
}

常量池 constant pool

JVM 常量池保证相同的字符串直接量只有一个,不会产生多个副本。

使用 new String() 创建的字符串对象是运行时创建出来的,被保存在运行时内存区(堆内存)。

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
public class StringCompareTest
{
public static void main(String[] args)
{
// s1直接引用常量池中的"233Java"
String s1 = "233Java";
String s2 = "233";
String s3 = "Java";
// s4后面的字符串值可以在编译时就确定下来
// s4直接引用常量池中的"233Java"
String s4 = "233" + "Java";
// s5后面的字符串值可以在编译时就确定下来
// s5直接引用常量池中的"233Java"
String s5 = "2" + "33" + "Java";
// s6后面的字符串值不能在编译时就确定下来,
// 不能引用常量池中的字符串
String s6 = s2 + s3;
// 使用new调用构造器将会创建一个新的String对象,
// s7引用堆内存中新创建的String对象
String s7 = new String("233Java");
System.out.println(s1 == s4); // 输出true
System.out.println(s1 == s5); // 输出true
System.out.println(s1 == s6); // 输出false
System.out.println(s1 == s7); // 输出false
}
}

equals() 方法

有时程序在判断两个引用变量是否想等时,也希望有一种类似于“值相等”的判断规则,并不严格要求两个引用变量指向同一个对象。

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

String 已经重写了该方法,只要两个字符串中所包含的字符序列相同,通过 equals() 比较将返回 true,否则将返回 false

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
class Person {
private String name;
private String idStr;
public Person(){;}
public Person(String name, String idStr) {
this.name = name;
this.idStr = idStr;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setIdStr(String idStr) {
this.name = idStr;
}
public String getIdStr() {
return this.idStr;
}
// 重写的 equals()
public boolean equals(Object obj){
// 如果为同一个对象
if (this == obj){
return true;
}
if (obj != null && obj.getClass() == Person.class) {
Person personObj = (Person)obj;
// 调用此方法的对象的 idStr 与 obj 对象的 idStr 相等才可判断两对象相等
if (this.getIdStr().equals(personObj.getIdStr())) {
return true;
}
}
return false;
}
}
public class OverrideEqualsTest {
public static void main(String[] args) {
Person p1 = new Person("鲁迅", "123");
Person p2 = new Person("周树人", "123");
Person p3 = new Person("鲁班", "002");
// p1 和 p2 的 idStr 相等,所以输出 true
System.out.println("p1 和 p2 是否相等?" + p1.equals(p2));
System.out.println("p1 和 p3 是否相等?" + p1.equals(p3));
}
}

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

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

类成员

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

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

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

在一些特殊情景下,要求不允许自由创建该类的对象,而只允许为该类创建一个对象。为了避免其他类自由创建该类的实例,应该把该类的构造器使用 private 修饰,从而把该类的所有构造器隐藏起来。

根据良好封装的原则:一旦把该类的构造器隐藏起来,就需要提供一个 public 方法作为该类的访问点,用于创建该类的对象,且该方法必须使用 static 修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)。

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
class Singleton {
// 使用一个类变量来缓存曾经创建的实例
private static Singleton instance;
// 将构造器使用 private 修饰,隐藏该构造器
private Singleton() {;}
// 提供一个静态方法,用于返回 Singleton 实例
// 该方法可以加入自定义的控制,保证只产生一个 Singleton 对象
public static Singleton getInstance() {
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();
// 将输出 true
System.out.println(s1 == s2);
}
}

通过 getInstance() 方法提供的自定义控制,保证 Singleton 类只能产生一个实例。所以在 main() 中,看到两次产生的 Singleton 对象实际上是同一个对象。

final 修饰符

final 关键字可用于修饰类、变量和方法,表明变量一旦获得了初始值就不可被改变。由于 final 变量获得初始值之后不能被重新赋值,因此 final 修饰成员变量和修饰局部变量时有一定的不同。

final 成员变量

对于 final 修饰的成员变量:如果既没有在定义成员变量时指定初始值,那么这些成员变量的值将一直是系统默认分配的 0, \u0000, falsenull,这些成员变量也就完全失去了存在的意义。因此 Java 语法规定:final 修饰的成员变量必须由程序员显式地指定初始值。与普通成员变量不同,系统不会对 final 进行隐式初始化,final 成员变量(包括实例变量和类变量)。

  1. 类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。
  2. 实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能在 3 个地方的其中之一指定。
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);
}
}

final 局部变量

不能对 final 修饰的形参赋值。详见下例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}
}

final 修饰引用类型变量

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

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

  1. 使用 final 修饰符修饰。
  2. 在定义该 final 变量时指定了初始值。
  3. 该初始量可以在编译时就被确定。

编译器会吧程序中所有用到该变量的地方直接替换成该变量的值。

final 方法

final 修饰的方法无法重写。

final

final 修饰的类不可以有子类,例如 java.lang.Math

不可变类

不可变 (immutable) 类的意思是,创建该类的实例后,该实例的实例变量是不可改变的。例如 Java 提供的 8 个包装类和 java.lang.String 类都是不可变类,这些类没有提供修改其实例变量的方法。

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

  1. 使用 privatefinal 修饰符来修饰该类的成员变量。尤其要注意引用变量,因为如果只是用 final 修饰引用变量,而没有用 private 保护引用变量不被外。
  2. 提供带参数构造器,用于提供传入参数来初始化类里的成员变量。
  3. 仅为该类的成员变量提供 getter 方法,不提供 setter 方法,因为普通方法无法修改 final 修改的成员变量。
  4. 如果有必要,重写 Object 类的 hashCode()equals() 方法。equals() 方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用 equals() 方法判断为相等的对象的 hashCode() 也想等。

缓存实例的不可变类

用一个数组来作为缓存池,从而实现一个缓存实例的不可变类。

抽象类

抽象方法和抽象类

  1. 均须用 abstract 来修饰。抽象方法不能有方法体。
  2. 抽象类不能实例化,无法用 new 关键字调用抽象类的构造器创建抽象类的实例。
  3. 抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接口、枚举)5 种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
  4. 含有抽象方法的类(包括直接定义了一个抽象方法;或继承了一个抽象父类,但没有完全实现父类包含的抽象方法;或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。

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

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
// Shape.java
public abstract class Shape {
private String color;
// 定义一个计算周长的抽象方法
public abstract double calPerimeter();
// 定义一个返回形状的抽象方法
public abstract String getType();
// 定义被子类调用的构造器
public Shape(){}
public Shape(String color) {
System.out.println("执行 Shape 的构造器...");
this.color = color;
}
public void setColor(String color) {
this.color = color;
}
public String getColor() {
return this.color;
}
}
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
// Triangle.java
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 "三角形";
}
}
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
// Circle.java
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);
}
// 重写 Shape 类的返回形状的抽象方法
public String getType() {
return getColor() + "圆形";
}
public static void main(String[] args) {
Shape triangle = new Triangle("黒色", 3, 4, 5);
Shape circle = new Circle("黄色", 3);
System.out.println(triangle.getType());
System.out.println(triangle.calPerimeter());
System.out.println(circle.getType());
System.out.println(circle.calPerimeter());
}
}

由于在 Shape 类中定义了 calPerimeter() 方法和 getType() 方法,所以程序可以直接调用 triangle 变量和 circle 变量的这俩个方法,无须强制类型转换为其子类类型。

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

使用模版模式的简单规则

  1. 抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类去实现。
  2. 父类中可能包含需要调用其他系列方法的方法,这些被调方法既可以由父类实现,也可以由其子类实现。父类里提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,而必须依赖于子类的辅助。

以下是一些注意要点:

访问权限

abstract 关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此 abstract 方法不能被定义为 private 访问权限。

成员变量与构造器

abstract 不能用于修饰成员变量,不能用于修饰局部变量,也不能用于修饰构造器。抽象类中定义的构造器也只能是普通构造器。

staticabstract

没有所谓的类抽象方法。因为使用 static 修饰一个方法时,可以通过该类调用该方法,但是如果该方法被定义成抽象方法,通过该类调用该方法会导致错误。

虽然 staticabstract 不能同时修饰某个方法,但是它们可以同时修饰内部类。

抽象方法和空方法体

注意区别:public abstract void test(); 是一个抽象方法,它根本没有方法体,即方法定义后面没有一对大括号;但 public void test(){} 方法是一个普通方法,它已经定义了方法体,只是方法体为空,因此这个方法不可用 abstract 来修饰。

接口

接口中不能包含普通方法,接口中所有方法都是抽象方法。Java 8 对接口进行了改进,允许在接口中定义默认方法,默认方法可以提供方法实现。

接口的好处是让规范和实现分离

接口的定义

基本语法

1
2
3
4
5
6
[修饰符] interface 接口名 extends 父接口 1, 父接口 2 ... {
零个到多个常量定义...
零个到多个抽象方法定义...
零个到内部类、接口、枚举定义...
零个到多个默认方法或类方法定义...
}
  1. 修饰符可以是 public 或者省略。
  2. 接口名与类名采用相同的命名规则。
  3. 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
  4. 注意:只有在 Java 8 以上的版本中才允许在接口中定义默认方法、类方法。
  5. 不管是否使用修饰符,接口里的普通方法总是使用 public abstract 来修饰。接口里的普通方法不能有方法实现(方法体);但类方法、默认方法都必须有方法实现。默认方法必须使用 default 修饰。
  6. 在接口中定义成员变量时,不管是否使用 public static final 修饰符,接口里的成员变量总是使用这三个修饰符来修饰。而且接口里没有构造器和初始化块,因此接口里定义的成员变量只能在定义时指定默认值。
  7. 接口里定义的内部类、内部接口、内部枚举默认采用 public static 修饰,无论定义时是否这样指定。

如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface Output {
// 接口里定义的成员变量只能是常量
int MAX_CACHE_LINE = 50;
// 接口里定义的普通方法只能是 public 的抽象方法
void out();
void getData(String msg);
// 在接口中定义默认方法,需要使用 default 修饰
default void print(String... msgs) {
for (String msg : msgs) {
System.out.println(msg);
}
}
static String staticTest() {
return "接口里的类方法";
}
}

使用接口

TODO:
接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类实现。归纳起来,接口主要有如下用途:

  1. 定义变量,也可用于进行强制类型转换。
  2. 调用接口中定义的常量。
  3. 被其他类实现。

一个类可以实现一个或多个接口,继承使用 extends,实现使用 implements 关键字。这也是 Java 为单继承灵活性不足所做的补充。

1
2
3
4
// public 也可替换成无修饰符、protected 或 private
public class MyClass extends Parent implements Interface {
类体部分
}

接口和抽象类

接口

接口作为系统与外界交互的窗口,接口体现的是一种规范。

对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供)。

对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务(就是如何来调用方法)。

当在一个程序中使用接口时,接口是多个模块间的耦合标准。

当在多个应用程序之间使用接口时,接口时多个程序之间的通信标准。

从某种程度上来看,接口类似于整个系统的“总纲”,它制定了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。

抽象类

作为系统中多个子类的共同父类,它所体现的是一种模版式设计。抽象类作为多个子类的抽象父类,是已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同的方式。

内部类

主要作用:

  1. 内部类提供了更好的封装,将内部类隐藏在外部类中,不允许同一个包中的其他类访问该类。
  2. 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
  3. 匿名内部类适合用于创建那些仅需要使用一次的类。

注意:

  1. 内部类比外部类可以多使用三个修饰符:private, protected, static——外部类不可以使用这三个修饰符。
  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
public class DiscernVariable
{
private String prop = "外部类的实例变量";
private class InClass
{
private String prop = "内部类的实例变量";
public void info()
{
String prop = "局部变量";
// 通过 外部类类名.this.varName 访问外部类实例变量
System.out.println("外部类的实例变量值:"
+ DiscernVariable.this.prop);
// 通过 this.varName 访问内部类实例的变量
System.out.println("内部类的实例变量值:" + this.prop);
// 直接访问局部变量
System.out.println("局部变量的值:" + prop);
}
}
public void test()
{
InClass in = new InClass();
in.info();
}
public static void main(String[] args)
{
new DiscernVariable().test();
}
}

如果外部类需要访问非静态内部类的成员,则必须显示创建非静态内部类对象来访问。

如果存在一个非静态内部类对象,则一定存在一个被它寄生的外部类对象。但外部类对象存在时,外部类对象里不一定寄生了非静态内部类对象。

根据静态成员不能访问非静态成员的规则,不允许在外部类的静态成员变量中直接使用非静态内部类。

1
2
3
4
5
6
7
8
9
10
11
12
public class StaticTest
{
// 定义一个非静态的内部类,是一个空类
private class In{}
// 外部类的静态方法
public static void main(String[] args)
{
// 下面代码引发编译异常,因为静态成员(main()方法)
// 无法访问非静态成员(In类)
new In();
}
}

Java 不允许在非静态内部类有静态方法、静态成员变量、静态初始化块。

使用内部类

在外部类内部使用内部类

和普通的类没有什么区别,只是不要在外部类的静态成员中使用非静态内部类。因为静态成员不能访问非静态成员。

在外部类以外使用非静态内部类

在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:

1
OuterClass.InnerClass varName

另外,如果外部类有包名,则还应该增加包名前缀。

局部内部类

很少使用。

如果把一个内部类放在方法里定义,这个内部类就是一个局部内部类,局部内部类仅在方法里有效,因此局部内部类不能使用访问控制符和 static 修饰符修饰。

Java 8 改进的匿名内部类

创建匿名内部类的格式如下:

1
2
3
4
new 实现接口 () | 父类构造器(实参列表)
{
// 匿名内部类的类体部分
}

匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或者实现一个接口。

匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。

匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。

最常用的创建匿名内部类的方式是需要创建某个接口类型的对象。

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
interface Product
{
public double getPrice();
public String getName();
}
public class AnonymousTest
{
public void test(Product p)
{
System.out.println("购买了一个" + p.getName()
+ ",花掉了" + p.getPrice());
}
public static void main(String[] args)
{
AnonymousTest ta = new AnonymousTest();
// 调用test()方法时,需要传入一个Product参数,
// 此处传入其匿名实现类的实例
ta.test(new Product()
{
public double getPrice()
{
return 567.8;
}
public String getName()
{
return "AGP显卡";
}
});
}
}

Product 只是一个接口,无法直接创建对象,因此此处考虑创建一个 Product 接口实现类的对象传入该方法。

当通过实现接口来创建匿名内部类时,匿名内部类也不能显示创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故 new 接口名后的括号里不能传入参数值。

但如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,即拥有相同的形参列表。

TODO:
在 Java 8 之前,Java 要求被局部内部类、匿名内部类访问的局部变量必须使用 final 修饰,从 Java 8 开始这个限制被取消了,Java 8 智能:如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了 final 修饰。

Java 8 将这个功能称为 “effectively final”。

参考

《疯狂 Java 讲义》第三版

您的支持将鼓励我继续创作!

热评文章