随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。
初始化和清理正是涉及安全的两个问题。
1. 用构造器确保初始化
在Java中,通过构造器,类的设计者可以确保每个对象都会得到初始化。创建对象时,如果类具有构造器,Java就会在用户操作对象之前自动调用相应的构造器,从而保证了初始化的进行。
构造器的特点:
- 构造器名称和类名完全相同
- 不接受任何参数的构造器叫做“默认构造器”或者“无参构造器”
- 如果没有创建任何构造器,编译器会默认创建一个“无参构造器”
- 可以自己创建构造器(有参无参都可以),但是这样编译器就不会创建“无参构造器”,如果需要使用,则必须自己创建
2. 方法重载
Java中构造器的名称必须和类名一致,也就是说只能有一个构造器名称,那么如果想用多种方式创建一个对象应该怎么办? 这就要用到方法重载。同时,尽管方法重载是构造器所必需的,但它也可用于其他方法。例如:
1 /** 2 * 重载 3 */ 4 public class Tree { 5 Tree() { 6 System.out.println("Tree"); 7 } 8 Tree(int height) { 9 System.out.println("Tree height = " + height); 10 } 11 12 void f() { 13 System.out.println("f()"); 14 } 15 16 void f(String s) { 17 System.out.println("f(String)"); 18 } 19 20 public static void main(String[] args) { 21 Tree t1 = new Tree(); 22 Tree t2 = new Tree(1); 23 t1.f(); 24 t1.f("test"); 25 26 } 27 } 28 输出: 29 Tree 30 Tree height = 1 31 f() 32 f(String)
2.1 区分重载的方法
通过参数列表:每个重载的方法都一个独一无二的参数类型列表,甚至不同的参数顺序也可以区分(这个要求所有的参数类型不能完全相同,尽量不要这么弄,会使代码难以维护)
疑问:是否可以通过返回值区分重载方法呢?
答案:不可以
如下面的重载函数,如果直接调用f(),编译器如何知道调用哪个方法。所以通过返回值区分重载方法是行不通的。
1 void f() { } 2 int f() {return 1;}
2.2 涉及基本类型的重载
如果传入的数据类型小于方法中声明的参数类型,实际数据类型会被提升。通过下面的代码可以看出基本类型向上转型的顺序:char->short->int->long->float->double;byte->short->int->long->float->double
1 /** 2 * 基本类型的重载 3 */ 4 public class PrimitiveOverloading { 5 6 void f1(char x) { System.out.print("f1(char) "); } 7 void f1(byte x) { System.out.print("f1(byte) "); } 8 void f1(short x) { System.out.print("f1(short) "); } 9 void f1(int x) { System.out.print("f1(int) "); } 10 void f1(long x) { System.out.print("f1(long) "); } 11 void f1(float x) { System.out.print("f1(float) "); } 12 void f1(double x) { System.out.print("f1(double) "); } 13 14 void f2(byte x) { System.out.print("f2(byte) "); } 15 void f2(short x) { System.out.print("f2(short) "); } 16 void f2(int x) { System.out.print("f2(int) "); } 17 void f2(long x) { System.out.print("f2(long) "); } 18 void f2(float x) { System.out.print("f2(float) "); } 19 void f2(double x) { System.out.print("f2(double) "); } 20 21 void f3(short x) { System.out.print("f3(short) "); } 22 void f3(int x) { System.out.print("f3(int) "); } 23 void f3(long x) { System.out.print("f3(long) "); } 24 void f3(float x) { System.out.print("f3(float) "); } 25 void f3(double x) { System.out.print("f3(double) "); } 26 27 void f4(int x) { System.out.print("f4(int) "); } 28 void f4(long x) { System.out.print("f4(long) "); } 29 void f4(float x) { System.out.print("f4(float) "); } 30 void f4(double x) { System.out.print("f4(double) "); } 31 32 void f5(long x) { System.out.print("f5(long) "); } 33 void f5(float x) { System.out.print("f5(float) "); } 34 void f5(double x) { System.out.print("f5(double) "); } 35 36 void f6(float x) { System.out.print("f6(float) "); } 37 void f6(double x) { System.out.print("f6(double) "); } 38 39 void f7(double x) { System.out.print("f7(double) "); } 40 41 void testChar() { 42 System.out.print("char: "); 43 char x = 'x'; 44 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 45 System.out.println(); 46 } 47 void testByte() { 48 System.out.print("byte: "); 49 byte x = 0; 50 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 51 System.out.println(); 52 } 53 void testShort() { 54 System.out.print("short: "); 55 short x = 0; 56 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 57 System.out.println(); 58 } 59 void testInt() { 60 System.out.print("int: "); 61 int x = 0; 62 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 63 System.out.println(); 64 } 65 void testLong() { 66 System.out.print("long: "); 67 long x = 0; 68 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 69 System.out.println(); 70 } 71 void testfloat() { 72 System.out.print("float: "); 73 float x = 0; 74 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 75 System.out.println(); 76 } 77 void testDouble() { 78 System.out.print("double: "); 79 double x = 0; 80 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 81 System.out.println(); 82 } 83 public static void main(String[] args) { 84 PrimitiveOverloading p = new PrimitiveOverloading(); 85 p.testChar(); 86 p.testByte(); 87 p.testShort(); 88 p.testInt(); 89 p.testLong(); 90 p.testfloat(); 91 p.testDouble(); 92 } 93 } 94 95 输出: 96 char: f1(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double) 97 byte: f1(byte) f2(byte) f3(short) f4(int) f5(long) f6(float) f7(double) 98 short: f1(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double) 99 int: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double) 100 long: f1(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double) 101 float: f1(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double) 102 double: f1(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double)
如果传入的实际参数**大于**重装方法声明的形式参数,需要通过类型转换,否则编译器会报错:
1 void f8(short x) { System.out.print("f8(short) "); } 2 // p.f8(5); //编译报错 3 p.f8((short)5);
3. this关键字
this表示“调用方法的那个对象”的引用,只能在方法内部使用:
- 可以在构造器中使用,调用其他的构造器
- 当方法的参数和成员变量名字相同时,可使用this关键字区分成员变量和传入参数
3.1 在构造器中调用构造器
可能为了一个类写了多个构造器,有时想在一个构造器中调用另一个构造器,以避免重复代码,可使用this关键字做到这点。在构造器中,如果为this添加了参数列表(空列表也可以),那么就会对符合此参数列表的某个构造器进行调用。
使用this调用构造器限制条件:
- 只能在构造器中使用this调用其他的构造器,其他方法不行
- 只能调用一次
- 必须是第一个被执行的语句
1 public class Flower { 2 int petalCount = 0; 3 String s = "initial value"; 4 5 Flower(int petals) { 6 petalCount = petals; 7 System.out.println("Constructor int arg only, petalCount = " + petalCount); 8 } 9 10 Flower(String s, int petals) { 11 12 this(petals); 13 // this(s); // 编译报错,只能调用一次 14 this.s = s; // 使用this关键字代表数据成员,防止和参数s混淆 15 System.out.println("String & int args"); 16 } 17 18 Flower() { 19 this("hi", 47); 20 System.out.println("default constructor (no args)"); 21 } 22 23 void pringPetalCount() { 24 // this(11); // 编译报错,只能在构造器中使用 25 System.out.println("petalCount = " + petalCount + " s = " + s); 26 } 27 28 public static void main(String[] args) { 29 Flower x = new Flower(); 30 x.pringPetalCount(); 31 } 32 } 33 34 输出: 35 Constructor int arg only, petalCount = 47 36 String & int args 37 default constructor (no args) 38 petalCount = 47 s = hi
3.2 static方法的含义
static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来可以。并且在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。
4. 清理
java中有垃圾回收器负责回收无用对象占据的内存,但是什么时候会执行垃圾清理不确定,有可能你创建的对象在程序运行过程中永远不会被清理:
- 对象可能不被垃圾回收
- 垃圾回收并不等于析构
- 垃圾回收只与内存有关
几种常见的垃圾清理机制:
垃圾清理机制 | 简要描述 | 缺陷 |
引用计数 | 简单但速度慢。每个对象都有一个引用计数,当有引用连接到对象时,引用计数加1。当引用离开作用域或者被置为null时,引用及时减一。当对象的引用计数为0时,释放其占用的空间。 | 对象之间存在循环引用,可能出现“对象应该被回收,但是引用计数却不为0” |
停止-复制 | 从堆栈和静态存储区出发,遍历所有对象引用,进而找出所有存活的对象。暂停程序的运行,将所有存活的对象拷贝到另一个堆,当对象被复制到新堆时,它们是一个挨着一个的。 | 效率低。首先需要两个堆,需要多维护一倍的空间。程序运行过程中可能只产生少量的垃圾,这种拷贝属于浪费 |
标记-清扫 | 从堆栈和静态存储区出发,遍历所有对象引用,进而找出所有存活的对象。每当找到一个存活对象会给对象一个标记,这个过程中不会回收任何对象,当全部标记工作方程时,才会清理。没有标记的对象被释放。 | 清理后剩余的空间是不连续的 |
5. 成员初始化
方法的局部变量不会给默认值,必须自己进行初始化;类的成员变量,会给默认值:
1 /** 2 * 3 */ 4 class Base { 5 6 } 7 public class DataInit { 8 boolean t; 9 char c; 10 byte b; 11 short s; 12 int i; 13 long l; 14 float f; 15 double d; 16 String ss; 17 DataInit reference; 18 19 void f() { 20 int j; 21 // System.out.println(j); // 编译报错 22 j = 1; 23 System.out.println("j = " + j); 24 } 25 26 void printInitValues() { 27 System.out.println("数据类型 默认初始化值"); 28 System.out.println("boolean " + t); 29 System.out.println("char #" + c + "#"); // char值为0,显示空白,可以使用(int)强制转化查看 30 System.out.println("byte " + b); 31 System.out.println("short " + s); 32 System.out.println("int " + i); 33 System.out.println("long " + l); 34 System.out.println("float " + f); 35 System.out.println("double " + d); 36 System.out.println("String " + ss); 37 System.out.println("reference " + reference); 38 } 39 public static void main(String[] args) { 40 DataInit d = new DataInit(); 41 d.printInitValues(); 42 } 43 } 44 45 输出: 46 数据类型 默认初始化值 47 boolean false 48 char # # 49 byte 0 50 short 0 51 int 0 52 long 0 53 float 0.0 54 double 0.0 55 String null 56 reference null
6. 构造器初始化
可以使用构造器初始化,即在运行时刻,可以调用方法或执行某些动作来确定初值。但要牢记:无法阻止自动初始化的进行,它将在构造器调用之前发生。如下代码,首先i会先被置0,然后变为7:
1 public class Counter { 2 int i; 3 Counter() { 4 i = 7; 5 } 6 }
6.1 初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。例如:
在House类中,故意把几个Window对象定义散布到各处,以证明它们全都会在调用构造器或其他方法之前得到初始化
1 /** 2 * 初始化的顺序 3 */ 4 5 class Window { 6 Window(int marker) { 7 System.out.println("Window(" + marker + ")"); 8 } 9 } 10 class House { 11 Window w1 = new Window(1); // 构造器之前 12 House() { 13 System.out.println("House"); 14 w3 = new Window(33); // 重新初始化 15 } 16 Window w2 = new Window(2); // 构造器之后 17 void f() { 18 System.out.println("f()"); 19 } 20 Window w3 = new Window(3); 21 } 22 public class OrderOfInit { 23 public static void main(String[] args) { 24 House h = new House(); 25 h.f(); 26 } 27 } 28 29 输出: 30 Window(1) 31 Window(2) 32 Window(3) 33 House 34 Window(33) 35 f()
6.2 静态数据初始化
无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能用于局部变量。如果一个域是静态的基本类型域,且没有初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么它的默认初值就是null。如果定义时初始化,和非静态数据没有区别。
1 /** 2 * 静态数据初始化 3 */ 4 class Bowl { 5 Bowl(int marker) { 6 System.out.println("Bowl(" + marker + ")"); 7 } 8 void f1(int marker) { 9 System.out.println("f1(" + marker +")"); 10 } 11 } 12 13 class Table { 14 Bowl bowl3 = new Bowl(3); 15 static Bowl bowl1 = new Bowl(1); 16 Table() { 17 System.out.println("Table()"); 18 bowl2.f1(1); 19 } 20 static void s() { 21 System.out.println("Table static method s()"); 22 } 23 void f2(int marker) { 24 System.out.println("f2(" + marker +")"); 25 } 26 static Bowl bowl2 = new Bowl(2); 27 } 28 //public class StaticInit { 29 // public static void main(String[] args) { 30 // System.out.println("Creating new Cupboard() in main"); 31 // Bowl bowl1 = Table.bowl1; 32 // } 33 //} 34 //输出: 35 //Creating new Cupboard() in main 36 //Bowl(1) 37 //Bowl(2) 38 39 //public class StaticInit { 40 // public static void main(String[] args) { 41 // System.out.println("Creating new Cupboard() in main"); 42 // Table.s(); 43 // } 44 //} 45 //输出: 46 //Creating new Cupboard() in main 47 //Bowl(1) 48 //Bowl(2) 49 //Table static method s() 50 public class StaticInit { 51 public static void main(String[] args) { 52 System.out.println("Creating new Cupboard() in main"); 53 table.f2(1); 54 } 55 static Table table = new Table(); 56 } 57 58 输出: 59 Bowl(1) 60 Bowl(2) 61 Bowl(3) 62 Table() 63 f1(1) 64 Creating new Cupboard() in main 65 f2(1)
由输出可见静态初始化只有在必要时刻才会进行,只有在一个Table对象被创建(或者第一次访问静态数据或静态方法,见注释代码部分)的时候,它们才会被初始化。此后静态变量不会初始化。初始化的顺序是先静态对象,后非静态对象。
对象创建的过程,假设有个名为Dog的类:
- 即使没有显式的使用static关键字,构造器实际上也是静态方法。因此当首次创建为Dog的对象时,或者Dog类的静态方法/静态域(静态成员变量)首次被访问时,Java解释器必须查找类的路径,以定位Dog.class文件
- 然后载入Dog.class(后面会学到),有关静态初始化的动作都会执行。因此静态初始化只在Class对象首次加载的时候进行一次
- 当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间
- 这块存储空间会被清零,这就自动将Dog对象中的所有基本类型数据设置成了默认值,而引用则被设置成了null
- 执行所有出现于字段定义出的初始化动作
- 执行构造器
6.3 显示的静态初始化
Java允许将多个静态初始化组成一个特殊的“静态初始化子句”(也叫“静态块”),例如:
1 class Cups { 2 static Cup cup1; 3 static Cup cup2; 4 // 静态子句(静态块) 5 static { 6 cup1 = new Cup(1); 7 cup2 = new Cup(2); 8 } 9 }
与其他静态初始化一样,静态子句仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于这个类的静态数据成员时。例如:
1 /** 2 * 显式的静态初始化 3 */ 4 class Cup { 5 Cup(int marker) { 6 System.out.println("Cup(" + marker +")"); 7 } 8 void f(int marker) { 9 System.out.println("f(" + marker +")"); 10 } 11 } 12 13 class Cups { 14 static Cup cup1; 15 static Cup cup2; 16 // 静态子句(静态块) 17 static { 18 cup1 = new Cup(1); 19 cup2 = new Cup(2); 20 } 21 Cups(){ 22 System.out.println("Cups()"); 23 } 24 } 25 26 public class ExplicitStatic { 27 public static void main(String[] args) { 28 System.out.println("Inside main()"); 29 Cups.cup1.f(99); 30 } 31 // static Cups cups1 = new Cups(); 32 } 33 34 输出: 35 Inside main() 36 Cup(1) 37 Cup(2) 38 f(99)
6.4 非静态实例初始化
Java中也有“实例初始化子句”,用来初始化每一个对象的非静态变量,例如:
1 /** 2 * 非静态实例初始化 3 */ 4 /** 5 * 非静态实例初始化 6 */ 7 class Mug { 8 Mug(int marker) { 9 System.out.println("Mug(" + marker +")"); 10 } 11 } 12 public class Mugs { 13 Mug mug1; 14 Mug mug2; 15 // 实例初始化子句 16 { 17 mug1 = new Mug(1); 18 mug2 = new Mug(2); 19 System.out.println("mug1 & mug2 init"); 20 } 21 Mugs() { 22 System.out.println("Mugs()"); 23 } 24 25 public static void main(String[] args) { 26 new Mugs(); 27 new Mugs(); 28 } 29 } 30 输出: 31 Mug(1) 32 Mug(2) 33 mug1 & mug2 init 34 Mugs() 35 Mug(1) 36 Mug(2) 37 mug1 & mug2 init 38 Mugs()
“实例初始化子句”相比“静态初始化子句”只少了static关键字,从输出可以看出实例初始化自己是在构造器之前执行的。
7. 数组初始化
定义:类型名+[],例如:
int[] a1;
方括号也可以放在标识符后面:
int a1[];
两种格式是一样的,推荐使用前一种,能明确表明定义的是“一个int类型的数组”,还有就是如果使用第二种定义有时与自己预期不符,比如想定义两个int数组,实际上只有a3是int数组,a4是整形变量:int a3[], a4;
编译器不允许指定数组的大小。因为创建的只是对数组的一个引用,并且没有给对象本身分配空间。数组初始化的几种方式:
- 定义的时候初始化: int[] a1 = {1, 2, 3}; // 这种方式只能在定义的时候使用
- 使用new创建,不赋初值: int[] a3 = new int[5]; // 这种方式可以先定义a3,在其他任何地方都可以初始化。因为没有赋初值,默认数组所有元素都为0
- 使用new创建,并赋初值: int[] a3 = new int[]{1, 2, 3, 4,5,}; // 这种方式同样可以先定义a3,在任何地方都可以初始化,其中初始化列表中的最后一个逗号可选(维护长列表方便,增加也方便)
所有数组都有length的成员,可以用来获取数组元素的个数: a.length
1 public class ListTest { 2 3 static String listToString(int[] a) { 4 String l = ""; 5 for (int i = 0; i < a.length; i++) { 6 l += a[i] + " "; 7 } 8 return l; 9 } 10 public static void main(String[] args) { 11 int[] a1 = {1, 2, 3, 4, 5}; 12 // int[] a1 = {1, 2, 3, 4, 5,}; //增加逗号也可以 13 int[] a2; 14 // a2 = {1, 2, 3, 4, 5}; // 编译报错,这种方式只能在定义时初始化 15 int[] a3 = new int[5]; // 使用new创建数组 16 int[] a4; // 先定义数组,在其他地方使用new初始化 17 int[] a5; // 先定义数组,在其他地方使用new初始化,并指定初值 18 a4 = new int[5]; 19 a5 = new int[]{1, 2, 3, 4,5,}; 20 System.out.println("a1 = " + listToString(a1)); 21 System.out.println("a3 = " + listToString(a3)); 22 System.out.println("a4 = " + listToString(a4)); 23 System.out.println("a5 = " + listToString(a5)); 24 } 25 } 26 输出: 27 a1 = 1 2 3 4 5 28 a3 = 0 0 0 0 0 29 a4 = 0 0 0 0 0 30 a5 = 1 2 3 4 5
7.1 可变参数
printArray函数可以接受0-n个任意对象, f函数至少接收一个整数,接收0-n个字符串:
1 public class NewArgs { 2 static void printArray(Object... args) { 3 for (Object obj : args) { 4 System.out.print(obj + " "); 5 } 6 System.out.println(); 7 } 8 static void f(int required, String... strs) { 9 System.out.print("requied: " + required + " "); 10 for (String s : strs) { 11 System.out.print(s + " "); 12 } 13 System.out.println(); 14 } 15 public static void main(String[] args) { 16 printArray(new Integer(47), new Float(3.14), new Double(11.11)); 17 printArray(47, 3.14f, 11.11); 18 printArray("one", "two", "three"); 19 printArray(); // 传空也可以 20 // f(); // 编译报错,至少传递一个参数 21 f(0); 22 f(1, "one"); 23 f(2, "one", "two"); 24 } 25 } 26 输出: 27 47 3.14 11.11 28 47 3.14 11.11 29 one two three 30 31 requied: 0 32 requied: 1 one 33 requied: 2 one two
8. 枚举类型
创建枚举类型Grade,具有A、B、C、D 4个值,枚举都是常量,使用大写字母表示(如果一个名字中有多个单词,用下划线分隔):
1 public enum Grade { 2 A, B, C, D 3 }
使用enum,需要创建一个该类型的引用,并将其赋值给某个实例:
1 Grade grade = Grade.A
创建enum时,编译器会自动增加一些有用的特性。例如,创建toString()方法,可以方便显示enum的实例名字;ordinal()方法,用来表示某个特定的enum常量的生命顺序;static values()方法,用enum常量的生命顺序,产生由这些常量构成的数组:
1 public class Ranking { 2 public static void main(String[] args) { 3 for (Grade grade : Grade.values()) { 4 System.out.println(grade + " ordinal " + grade.ordinal()); 5 } 6 } 7 } 8 输出: 9 A ordinal 0 10 B ordinal 1 11 C ordinal 2 12 D ordinal 3
还可用于switch语句:
1 // Grade.java 2 public enum Grade { 3 A, B, C, D 4 }
1 //Ranking.java 2 public class Ranking { 3 Grade grade; 4 Ranking(Grade grade) { 5 this.grade = grade; 6 } 7 public void getRankingDes() { 8 switch (grade) { 9 case A: 10 System.out.println("优秀"); 11 break; 12 case B: 13 System.out.println("良好"); 14 break; 15 case C: 16 System.out.println("及格"); 17 break; 18 case D: 19 System.out.println("不及格"); 20 break; 21 default: 22 System.out.println("错误输入"); 23 } 24 } 25 public static void main(String[] args) { 26 Ranking rank1 = new Ranking(Grade.A); 27 Ranking rank2 = new Ranking(Grade.D); 28 rank1.getRankingDes(); 29 rank2.getRankingDes(); 30 } 31 } 32 33 输出: 34 优秀 35 不及格