目录
通过构造器进行初始化
无参构造器
方法的重载
使用基本类型的重载
this关键字
在构造器中调用构造器
static的含义
成员初始化
初始化顺序
静态数据的初始化
显式的静态初始化(静态块)
非静态实例的初始化
数组初始化
动态数组的创建
可变参数列表
清理
finalize()的特殊用法
垃圾收集器的工作原理
枚举类型
局部变量类型判断
本笔记参考自: 《On Java 中文版》
初始化和清理的不到位,很容易导致编程上的“不安全”。Java引入了构造器,以此来保证对象的初始化,同时通过垃圾收集器来回收内存的资源。
通过构造器进行初始化
Java中,类的设计者可以通过编写构造器来确保每一个对象的初始化。当一个类中存在构造器时,Java就会在创建该对象时自动调用它。而为了防止构造器的命名与类中的成员冲突,Java规定:构造器的名字就是类的名字。例如:
class Cla {
Cla() { // 和类名一致,这就是一个构造器
System.out.println("这是一条信息,表示构造器被执行。");
}
}
public class SimpleConstructor {
public static void main(String[] args) {
for (int i = 0; i < 10; i++)
new Cla();
}
}
程序运行的结果如下:
上述程序使用循环创建对象,当语句new Cla();被执行时,会发生两件事:① 为这一对象分配空间;② 调用这个类的构造器。Java通过构造器保证一个对象在被使用之前已经被正确地初始化了。
构造器的命名就是类名,是大小写敏感的。
构造器可以被分为两种:
- 默认构造器:也称为零参数构造器,这种构造器不带有参数;
- 含参构造器:和方法类似,构造器也可以通过传入参数来指定创建对象的方式。这种构造器一般由程序员编写。
使用含参数的构造器的方式与上述例子类似:
class Cla2 {
Cla2(int i) {
System.out.println("这是第" + (i + 1) + "次的初始化");
}
}
public class SimpleConstructor2 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++)
new Cla2(i);
}
}
程序运行的结果是:
若一个对象只有唯一的一个构造器,那么编译器不会运行通过任何其他方法进行对象的创建。
注意:在Java中,创建和初始化是一个统一的概念,二者是缺一不可的。
构造器是一种特殊的方法,这种方法没有返回类型。这和返回类型为空(void)是不同的,对于空返回类型而言,虽然方法不会返回任何内容,但是仍然可以修改返回类型,使其存在一个返回值。但构造器不同,无法进行这种修改。
构造器常被用于对象的初始化。但对类而言,所有的基本类型的对象引用都会被自动初始化,这一过程是先于构造器的,因此使用构造器进行初始化不是强制的。
无参构造器
之前提到过,无参构造器,即没有参数的构造器,通常被用于创建“默认对象”。另外,即使我们没有为自定义的类创建构造器,编译器也会自动为这个类添加一个无参构造器。但是,如果我们已经创建了一个构造器,那么编译器就不会再自动进行无参构造器的创建了。例如:
class Tmp {
Tmp(int i) {
}
Tmp(double d) {
}
}
public class NoSynthesis {
public static void main(String[] args) {
// Tmp t = new Tmp();
Tmp t1 = new Tmp(1);
Tmp t2 = new Tmp(1.0);
}
}
若在上述程序中,执行被注释的语句Tmp t = new Tmp();的话,编译器会提示找不到匹配的构造器,并进行报错:
方法的重载
在编程语言中,命名是一个很重要的特性,比如方法就是对动作的命名。重载的含义在于,同一个词可以表达多种不同的含义,这更加接近我们使用语言的习惯。
Java之所以需要使用重载,有两个原因:
- 一个原因是因为不是每一个元素都需要一个唯一的标识符(即使省略一些不必要的词汇,语义依旧是连贯的);
- 而另一个原因,就是因为构造器。在编程中,我们可能会需要通过不同的方式进行对象的创建,这就需要多个构造器。但无论有几个构造器,它们都只会有一个相同的名字:类名。为此,就需要使用方法重载来区分不同的构造器。
构造器和方法的重载用法都较为简单:
class Tree {
int height;
Tree() {
System.out.println("这是一颗树苗");
height = 0;
}
Tree(int init) {
height = init;
System.out.println("创建一棵高度为" + height + "米的树");
}
void info() {
System.out.println("树的高度是" + height + "米");
}
void info(String s) {
System.out.println(s + ":树的高度是" + height + "米");
}
}
public class Overloading {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Tree t = new Tree(i);
t.info(); // 调用重载的方法
t.info("系统提示");
System.out.println("--------------------------");
}
System.out.println(); // 调用重载的构造器
new Tree();
}
}
上述程序执行的结果是:
区分重载
重载中,不同的方法有相同的名字,为了区分它们,规定:每一个重载方法都必须有独一无二的参数类型列表。(实际上,就算只是参数顺序的不同也可以区分方法,但是这样产生的代码通常更难维护。)
通过返回值无法区分重载。这是因为有时我们并不关心返回值,而仅仅只是调用了该方法,此时Java可能无法区分有返回值的方法和无返回值的方法。因此,不使用返回值区分重载。
使用基本类型的重载
已知,基本类型可以从较小的类型自动提升到较大的类型。这一特性有时也会发生重载中:
public class PrimitiveOverloading {
void f1(char x) {
System.out.print("f1(char) ");
}
void f1(int x) {
System.out.print("f1(int) ");
}
void f2(int x) {
System.out.print("f2(int) ");
}
void testChar() {
char x = 'x';
System.out.print("char: ");
f1(x);
f2(x);
System.out.println();
}
void testint(){
int x = 0;
System.out.print("int: ");
f1(x);
f2(x);
System.out.println();
}
public static void main(String[] args) {
PrimitiveOverloading p = new PrimitiveOverloading();
p.testChar();
p.testint();
}
}
程序运行的结果如下:
注意,方法f2()的重载中并没有关于char类型的参数列表。因此char在f2()中并没有被精确匹配,而是被提升为了int类型。除此之外,若传入的数据类型比重载方法的参数类型大,就必须使用窄化转型,否则编译器会发生报错。
this关键字
当多个同一类型的对象分别调用同一个方法时,编译器会进行一些幕后工作,以此保证被调用的方法能够知道自己是被谁调用。这就是隐藏参数。例如:
class Banana{
void peel(int i ){
// ...
}
}
public class BananaPeel {
public static void main(String[] args) {
Banana a = new Banana();
Banana b = new Banana();
a.peel(1); //a、b调用了同一个方法
b.peel(2);
}
}
在上述的程序中,方法peel()有一个隐藏参数,位于所以参数之前,代表着被操作对象的引用。所以,上述a和b调用方法的语句也可以写成:
Banana.peel(a, 1);
Banana.peel(b, 2);
当然,我们不能通过这种形式进行程序的编写。
上述这个被隐藏的引用是无法通过一般方式进行使用的,因为它根本不在参数列表中。这时就需要使用到一个Java提供的关键字:this(该关键字只能在非静态方法中使用)。这一关键字就代表了对象的引用,可以像使用任何其他对象一样使用this。
另外,若在类的一个方法内部调用该类的另一个方法,并不需要使用this:
public class Apricot {
void pick() {
// ...
}
void pit() {
pick(); // 此处可以直接调用pick()
// ...
}
}
上述代码的语句pick();原本应该是this.pick();,但这样写是不必要的。因为编译器会自动进行补充。需要用到this的场景一般是:当必须明确指出当前对象的引用时。例如:
public class Leaf {
int i = 0;
Leaf increment() {
i++;
return this;
}
void print() {
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print();
}
}
上述程序的输出结果是:i = 3。因为increment()方法会返回当前对象的引用,因此可以轻易执行对同一对象的若干操作。
this本身也可以被作为参数传递到其他方法中:
class Person {
public void eat(Apple apple) {
Apple peeled = apple.getPeeled();
System.out.println(("有人吃了一个苹果"));
}
}
class Peeler {
static Apple peel(Apple apple) {
return apple;
}
}
class Apple {
Apple getPeeled() {
return Peeler.peel(this);
}
}
public class PassingThis {
public static void main(String[] args) {
new Person().eat(new Apple());
}
}
程序执行后,会显示一行输出:有人吃了一个苹果。在上述的代码中,类Apple调用了外部的方法Peeler.peel(),此时为了将自身传递出去,就需要使用this。
在构造器中调用构造器
若一个类有多个构造器,为了避免代码的重复,就需要从一个构造器调用另一个构造器。这种情况就可以使用this关键字进行调用。
在构造器中,this关键字可以有两个含义:
- 即通常的含义,表示对当前对象的引用;
- 若在this后加上参数列表,此时的this会显式调用与参数列表相匹配的构造器。
下面是一个调用其他构造器的例子:
public class Invoke {
int count = 0;
String s = "初始值";
Invoke(int x) {
count = x;
System.out.println("该构造器只有一个int类型的参数,count = " + count);
}
Invoke(String ss) {
System.out.println("该构造器只有一个String类型的参数,初始值s = " + s);
}
Invoke(String s, int x) {
this(x);
// this(s); // 不能同时调用两个构造器
this.s = s;
System.out.println("该构造器有String类型和int类型的参数");
}
Invoke() {
this("Hello World", 100);
System.out.println("无参构造器被调用");
}
void printCount() {
// this(11); // 不能在非构造器中调用其他构造器
System.out.println("count = " + count + ", s = " + s);
}
public static void main(String[] args) {
Invoke i = new Invoke();
i.printCount();
}
}
程序执行的结果是:
虽然可以通过this调用另一个构造器,但是不能同时调用两个。并且,构造器的调用必须出现在方法的最开始部分,否则会报错。
在构造器语句Invoke(String s, int x)中,有一个参数s和成员s名字相同。为了防止产生歧义,可以使用this.s来表示成员数据。
最后,编译器禁止在非构造器的普通方法中调用构造器。
static的含义
一个带有static关键字的方法是没有this的。这种方法可以在没有创建对象的时候,通过类直接进行调用(Java中不允许使用全局方法)。一个类的静态方法可以访问其他静态方法和静态字段。
然而,静态方法本身并不符合面向对象的思想。所以在设计静态方法之前,需要好好考虑。
成员初始化
Java对于变量的初始化有很严格的要求。对于方法的局部变量,若未进行初始化,就会出现报错。例如:
public class Tmp {
public static void main(String[] args) {
int i;
i++;
}
}
若编译上述程序,会发生编译时错误:
上述信息指出变量i未被初始化。尽管编译器可以赋予这种变量默认值,但强制程序员进行初始化很显然更加安全。另外,之前也提到过,如果类的字段是基本类型,那么这些字段都会得到一个初始值。例如:
public class InitialValues {
boolean t;
char c;
byte b;
int i;
void printInitalValues() {
System.out.println("数据类型 默认值");
System.out.println("boolean " + t);
System.out.println("char " + c);
System.out.println("byte " + b);
System.out.println("int " + i);
}
public static void main(String[] args) {
new InitialValues().printInitalValues();
}
}
程序运行的结果如下:
即使没有指定初始值,类的字段也会被自动初始化(char类型的初始值为0,但笔者的系统进行输出时未显示)。这种做法规避了未初始化变量的风险。
若未进行初始化的字段是一个对象的引用,那么这个引用会被赋予一个特殊的初始值:null。
还可以通过方法来提供初始值,但方法的参数必须是已经初始化的:
public class MethodInit {
int i = f();
int j = g(i);
int f() {
return 1;
}
int g(int n) {
return n * 2;
}
}
但是,Java不允许使用前向引用,这意味着Java对初始化的顺序做出了明确要求。所以如果将上述代码稍加修改:
public class MethodInit {
int j = g(i);
int i = f();
int f() {
return 1;
}
int g(int n) {
return n * 2;
}
}
就会得到一个警告:
使用默认的初始化有时不会得到我们想要的结果。当进行手动的初始化时,我们就能得到更大的灵活性。
初始化顺序
类中的变量定义的顺序会决定初始化的顺序。即使将定义分散到方法之间,变量定义依旧会在任何方法(包括构造器)调用之前被初始化。例如:
class Window {
Window(int i) {
System.out.println("Window(" + i + ")");
}
}
class House {
Window w1 = new Window(1); // 在构造器之前被定义
House() { // 构造器
System.out.println();
System.out.println("已进入构造器House");
w3 = new Window(3); // w3在构造器之后被定义
}
Window w2 = new Window(2);
void f() { // 方法
System.out.println();
System.out.println("调用方法f()");
}
Window w3 = new Window(3);
}
public class OrderOfInitialization {
public static void main(String[] args) {
House h = new House();
h.f();
}
}
上述程序执行的结果如下:
在上述程序中,对Window对象的定义被分散到了方法之间,但显然它们在所以方法被调用之前就已经被初始化了。其中,对象w3被初始化了两次,一次在构造器被调用之前,一次在构造器中。一般建议在使用对象时再进行一次初始化,以保证正确的初始化。
静态数据的初始化
对于类而言,无论创建了多少个该类的对象,这个类中的静态数据都只会占用一份存储空间。static关键字只能对字段使用,而不能用于局部变量。并且静态的字段也会被初始化。例如:
class Bowl {
Bowl(int market) {
System.out.println("Bowl(" + market + ")");
}
void f1(int marker) {
System.out.println("f1(" + market + ")");
}
}
class Table {
Bowl bowl1 = new Bowl(1); // 这是一个非静态的变量
Table() {
System.out.println("调用构造器Table()");
bowl2.f1(1);
}
void f2(int marker) {
System.out.println("f2(" + market + ")");
}
static Bowl bowl2 = new Bowl(2);
}
public class StaticInitialization {
public static void main(String[] args) {
table.f2(1);
}
static Table table = new Table();
}
上述程序执行的结果如下:
static初始化仅在必要的时候发生。在上述例子中,若不创建Table对象,并且不引用Table.bowl1或Table.bow2,那么Bowl类型的bowl1和bowl2将永不被创建。
若一个类存在静态成员,那么当第一次创建该类的对象,或者第一次访问该类的静态成员时,该类的所有静态数据将被初始化。(实际上,构造器也是一个静态方法。)
||| 初始化的顺序:静态字段 → 非静态字段。
显式的静态初始化(静态块)
静态子句,又称静态块。在一个类中,可以将多个静态初始化语句放在一个“静态子句”中,例如:
public class Spoon {
static int i;
static {
i = 47;
}
}
这段由static开头的语句块,与其他的静态初始化语句有一样的功能只执行一次,即使之前没有创建过该类的对象。例如:
class Day {
Day(int marker) {
System.out.println("Day(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Days {
static Day day1;
static Day day2;
static {
day1 = new Day(1);
day2 = new Day(2);
}
Days() {
System.out.println("Days()");
}
}
public class ExplicitStatic {
public static void main(String[] args) {
System.out.println("进入main()");
Days.day1.f(99); // [1]
}
// static Days days1 = new Days(); // [2]
// static Days days2 = new Days(); // [2]
}
程序运行的结果是:
其中,无论是[1]还是[2],只要有其中一段语句存在,Cups的静态初始化就会发生。
非静态实例的初始化
对于对象的非静态变量,Java提供了一种名为实例初始化的类似语法:
class Mug {
Mug(int maker) {
System.out.println("Mug(" + maker + ")");
}
}
public class Mugs {
Mug mug1;
Mug mug2;
{ // 实例初始化
mug1 = new Mug(1);
mug2 = new Mug(2);
System.out.println("mug1 和 mug2 完成初始化");
}
Mugs() {
System.out.println("Mugs()");
}
Mugs(int i) {
System.out.println("Mugs(int)");
}
public static void main(String[] args) {
System.out.println("进入main()方法");
System.out.println("-----------------");
new Mugs();
System.out.println("完成Mugs()操作");
System.out.println();
new Mugs(1);
System.out.println("完成Mugs(1)操作");
}
}
程序执行的结果是:
该语法可用①于支持匿名内部类的初始化,以及②用于确保无论调用哪个显式的构造器,一些操作都会发生。
数组初始化
Java可以通过两种语法定义数组:
int[] a1;
int a1[]; // C和C++的语法,但是Java也可以用
在Java中,编译器不被允许指定数组的大小。当我们通过上述的方式使用数组时,我们还仅仅是拥有了这个数组的引用。但数组对象本身的空间却没有被分配。因此,就需要进行初始化。数组可以在代码的任何地方被初始化。而在创建数组时进行初始化有特殊的语法,例如:
int[] a1 = {1, 2, 3, 4};
另外,由于a1是一个数组的引用,因此也存在下方的用法:
int a2[] = {1, 2};
a1 = a2;
在Java中,所有的数组都有一个固定的成员length,它可以查询数组内的元素个数。同时,Java中的数组也是从元素0开始计数,因此数组索引的最大下标值为length - 1。
但是,Java的数组是不允许越界访问的。
动态数组的创建
可以使用new关键字创建数组中的元素,例如:
import java.util.Arrays;
import java.util.Random;
public class ArrayClassObj {
public static void main(String[] args) {
Random rand = new Random(47);
Integer[] a = new Integer[rand.nextInt(20)];
System.out.println("数组a的长度为" + a.length);
for (int i = 0; i < a.length; i++)
a[i] = rand.nextInt(500); // 自动装箱
System.out.println(Arrays.toString(a));
}
}
程序运行的结果是:
当使用语句Integer[] a = new Integer[rand.nextInt(20)];之后,我们可以得到一个元素是引用的数组。此时这些引用都没有关联上任何元素。直到通过后面的自动装箱为每一个引用进行初始化后,才真正完成这个数组的初始化。
若未创建对象,而试图直接使用数组中的引用,程序就会出错。
通过花括号,还可以通过下述语法初始化对象数组:
Integer[] a = { 1, 2, 3, }; //最后的逗号是可选的
Integer[] b = new Integer[] { 1, 2, 3, };
通过上述对数组b初始化的方式,还可以将一个String类型的数组传递给另一个类的main()方法:
public class DynamicArray {
public static void main(String[] args) {
Other.main(new String[] { "你", "好", "呀" });
}
}
class Other {
public static void main(String[] args) {
for (String s : args)
System.out.print(s + " ");
System.out.println();
}
}
程序执行的结果如下:
可变参数列表
Java的可变参数列表包括:数量可变的参数和未知类型的参数。一种使用方式是通过Java的公共根类Object,可以创建一个接受Object数组的方法,例如:
import java.rmi.server.ObjID;
class A {
}
public class VarArgs {
static void printArray(Object[] args) {
for (Object obj : args)
System.out.print(obj + " ");
System.out.println();
}
public static void main(String[] args) {
printArray(new Object[] { 12, (float) 3.14, 1, 11 });
printArray(new Object[] { "Hello", "World" });
printArray(new Object[] { new A(), new A(), new A() });
printArray((Object[]) new Integer[] { 1, 2, 3, 4 }); // 通过强制类型转换,将数组作为列表传输
}
}
程序执行的结果如下:
在第三行输出中,可以发现打印出来的类名都是@符号后面跟着十六进制数字。这就是在没有处理情况下,print()的默认行为:打印类名和对象的地址。
除了上述的可变参数列表的表达方式外,还可以使用省略号定义一个可变参数列表:
static void printArray(Object... args) { // ...
当然,无论通过哪种方式,最终得到的args都会是一个数组。
但是,若是想要传递空参数列表(不传递参数,类似于printArray();)时,需要使用带有省略号的语法,即第二个语法。
下面是一个使用省略号的可变参数列表的例子,这种参数列表在面对可选的尾随参数时很有用:
public class OptionalTrailingArguments {
static void f(int required, String... trailing) { // 一个非Object类型的可变参数列表
System.out.print("required: " + required + " ");
for (String s : trailing)
System.out.print(s + " ");
System.out.println();
}
public static void main(String[] args) {
f(1, "多出来的字符");
f(2, "多出来了", "很多字符");
f(3);
}
}
程序运行的结果如下:
在可变参数列表中,可以使用任何类型的参数。另外,可变参数列表也可以像数组一样触发自动装箱机制,因此可以对这种参数使用for-in语句:
public class Var {
public static void f(Integer... args) {
for (for i : args)
// ...
}
}
但是,因为这种参数列表的加入,重载过程会变得更加复杂。当有多个带有可变参数列表的类存在时,编译器可能会因此找不到目标。例如:
public class OverloadingVarargs {
static void f(Character... args) {
System.out.println("这个重载有一个Character类型的可变参数列表");
}
static void f(Integer... args) {
System.out.println("这个重载有一个Integer类型的可变参数列表");
}
public static void main(String[] args) {
f(1);
f('a');
f(); // 编译器不明白这到底对应着哪一个重载
}
}
若试图编译上述程序,会得到报错:
为了解决上述的这种问题,可能会为某个方法添加一个非可变参数(但这样还不够)。尝试改写上面的列子:
public class OverloadingVarargs {
static void f(float t, Character... args) {
System.out.println("这个重载多了一个float类型的变量");
}
static void f(Character... args) {
System.out.println("这个重载有一个Character类型的可变参数列表");
}
public static void main(String[] args) {
f(1, 'a');
f('a', 'b');
}
}
即使如此,还是无法顺利运行代码。对上述程序而言,参数列表还是存在歧义的:
比较常见的做法是为每一个方法都添加一个非可变参数:
public class OverloadingVarargs {
static void f(float t, Character... args) {
System.out.println("这个重载多了一个float类型的参数");
}
static void f(char c, Character... args) {
System.out.println("这个重载多了一个char类型的参数");
}
public static void main(String[] args) {
f(1, 'a');
f('a', 'b');
}
}
不过最好的办法是在一个重载上面使用可变参数列表,或者更不不用。
清理
Java的垃圾收集器可以回收不再被使用的对象内存,但有一些特殊情况除外:对象不使用new进行分配,而垃圾收集器只能释放由new分配的内存,此时这块空间就得不到回收。为此,Java允许在类中定义一个名为finalize的方法。
若存在finalize()方法,那么回收内存的动作应该是:
- 调用finalize()方法;
- 在下一次垃圾收集动作发生时,回收内存。
finalize()方法并不对标C++中的析构函数。
Java中的清理有这样的一些特点:
- 对象可能不会被回收:当程序在运行时还没有耗尽储存空间时,一些对象存储空间可能不会被回收(因为垃圾收集本身也会有开销);
- 垃圾收集不是析构;
- 垃圾收集只和内存有关:任何与垃圾收集有关的动作(包括finalize()方法),都必须与内存及其的回收有关。
调用finalize()方法的时机
一般情况下,finalize()方法是不需要被调用的,因为垃圾收集器就已经可以完成大部分的内存回收工作。但正如上面所述,若对象是以某种特殊方式被分配了存储空间,此时就会用到finalize()方法。
之所以会发生这种情况,是因为Java允许调用本地方法,而通过本地方法又可以调用非Java代码。Java的本地代码目前只支持C和C++,但C和C++又能调用其他方法。所以,Java实际上可以调用的代码范围十分广泛。
为了处理这种可能的由非Java代码分配的空间,就会需要使用finalize()方法进行处理。
必须执行的清理
Java不允许创建本地对象,任何对象的创建都离不开new。并且,Java并不存在用于释放对象的操作符(像delete),这是因为垃圾收集器可以处理这一切。但除了内存释放之外,还可能存在其他的清理操作。此时就需要显式调用恰当的方法,这些方法类似于C++的析构函数。
finalize()的特殊用法
finalize()本身并不依赖于每次都被调用。因此,可以通过这一特性对程序进行一定的检测:
class Book {
boolean checkedOut = false;
Book(boolean checkOut) {
checkedOut = checkOut;
}
void checkIn() {
checkedOut = false;
}
@SuppressWarnings("deprecation") // 用于禁止在JDK 8以上的版本在编译时通过警告信息
@Override
public void finalize() {
if (checkedOut)
System.out.println("错误:检测失败");
// super.finalize(); // 调用基类版本
}
}
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
novel.checkIn(); // 通过checkIn()进行正常的清理流程
new Book(true); // 引用丢失
System.gc(); // 向JVM请求进行垃圾收集和finalize()
}
}
程序执行后,打印结果:错误:检测失败。通过这种方式,就可以对未正确清理的对象进行验证,以提供更多的错误信息。
另外,在上述代码中出现了@Override。其中,@表示注解,用于提供代码的额外信息。在这里,@Override是为了告诉编译器,我们要自定义finalize()方法。并且由于finalize()方法在JDK 8之后不被推荐使用,所以需要用@SuppressWarnings("deprecation")来屏蔽警告。
应该假设finalize()的基类版本做了某些重要的工作,因此要使用super.finalize();。由于异常处理,上述例子为使用该语句。
垃圾收集器的工作原理
对许多编程语言而言,在堆上分配对象需要较大的代价。但对于Java而言,由于垃圾收集器的存在,其在堆上分配储存的速度,不亚于其他语言在栈上分配储存的速度。
这种看起来挺神奇的操作源于一些Java虚拟机(JVM)的工作方式。对于Java而言,在堆上分配空间只意味着让“堆指针”简单地移动到尚未分配的区域。因此它的效率可以与C++的栈分配相媲美。
当然,如果原理仅仅只是那么简单,那么内存终究有被耗尽的时候。当“堆指针”过度远离其的起始位置时,就有可能发生缺页错误。这就需要垃圾处理器的介入了:在回收垃圾的同时,垃圾处理器会压缩堆中的空间,让“堆指针”重新往起点靠近。由此,就形成了一个高速、且空间巨大的堆模型。
垃圾收集器会通过不同的方案提高回收垃圾收集的速度。一些有多个方案的JVM还会使用一种自适应的垃圾收集方案。
除了垃圾收集方案以外,Java也有其他的附加技术用以提升速度。例如编译器会将程序部分或者全部编译为本地机器码。
枚举类型
Java 5添加了enum关键字,使得Java编程能够使用枚举类型。例如:
public enum Color {
BLUE, RED, YELLOW
}
枚举类型的实例都是常量,因此一般使用全大写的形式。
使用枚举的方式也很简单,只需要创建引用,并且分配其的实例即可。例如:
Color col = Color.BLUE;
另外,Java为枚举类型添加了一些方法,例如:
- toString()方法,可用于显示enum实例的名字,在打印枚举类型时这个方法也会被自动调用;
- ordinal()方法,表示特定enum常量的声明顺序;
- values()方法,按照声明顺序生成一个enum常量值的数组;
- ......
这些方法的使用方式可参考下方的例子:
public class EnumOrder {
public enum Color {
BLUE, RED, YELLOW
}
public static void main(String[] args) {
for (Color col : Color.values())
System.out.println(col + ", ordinal: " + col.ordinal());
}
}
程序运行的结果如下:
同时,枚举类型也经常在switch语句中被使用。
局部变量类型判断
在JDK 10中加入了一个新的特性:类型推断。在JDK 11中,这一用来简化局部变量定义的特性得到了改进。通过var关键字就可以启用:
class Tmp {
}
public class TypeInfrence {
void meth() {
String s1 = "Hello";
var s2 = "Hello"; // 可用于显式类型
Tmp t1 = new Tmp();
var t2 = new Tmp(); // 也可用于用户定义的类型
}
static void staticMethod() { // 也可用于静态方法
var s3 = "Hello";
var t3 = new Tmp();
}
}
但是,var在使用时也存在一些限制,例如:
- 不能在字段上使用类型推断,例如:
class NoInference { String field1 = "可以这样做"; var field2 = "不能这样做"; }
其中,field2的初始化方式就是不被允许的;
- 必须提供初始化数据,但不能是null,例如:
class NoInference { void method() { var noInitializer; // 初始化数据不存在,报错 var aNull = null; // 初始化数据为null,报错 } }
- 不能用于方法的参数,例如:
class NoInference { void method(var no) { //参数为空,报错 // ... } }
类型推断多用于for循环,下面是一个正确使用类型推断的例子:
public class ForTypeInference {
public enum Color {
BLUE, RED, YELLOW
}
public static void main(String[] args) {
for (var col : Color.values())
System.out.println(col);
}
}
Java由于向后兼容的关系,类型推断受到限制。在使用的时候,可以先进行尝试,然后让编译器来提示是否可行。