一起读Java编程思想(2)---构造器的初始化与清理

    科技2024-12-15  14

    初始化与清理

    用构造器确保初始化

    每个类都要定义一个initialize()方法,提醒在使用对象之前必须调用这个方法,使得类的设计者可以确保每个对象都可以被初始化。

    构造函数是没有返回类型的函数,用于构造函数,同时这种构造函数是用来初始化的。构造器是一种特殊的方法,构造器也是可以重载的。

    class Tree { int height; Tree() { print("Planting a seedling"); height = 0; } Tree(int initialHeight) { height = initialHeight; print("Creating new Tree that is " + height + " feet tall"); } void info() { print(s + ":Tree is " + height + " feet tall"); } void info(String s) { print(s + ": Tree is " + height + " feet tall."); } } public class Overlaading { public static void main(String[] args) { for(int i = 0; i < 5; i++) { Tree t = new Tree(i); t.info(); t.info("overloaded method"); } //重载 new Tree(); } }

    构造器是一种特殊的方法,没有返回值,与返回值为空void不同,空返回值,尽管方法本身不会自动返回,但是仍然可以选择让他返回别的东西。加入构造器具有返回值,并且允许自行选择返回类型,要让编译器知道如何处理值。

    方法重载

    Java中,构造器是强制重载方法名的一个原因。既然构造器的名字已经由类名觉得,只能有一个构造器的名字。如果想要有多种方式创建一个对象,那么就需要两个构造器:一个是默认构造器,另外一个是其他方法的构造器。为了让方法名称相同但是形式的参数不同的构造器同时存在,就必须使用方法重载。

    区分重载的方法,只需要让每个重载的方法的参数类型列表独一无二。

    this关键字

    在方法的内部获得对当前对象的引用使用this, this只能在方法的内部使用,表示对"调用方法的那个对象"的引用。注意:如果在方法内部调用同一个类的另外一个方法,就不用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

    此处increament()通过this关键字返回对当前对象的引用,容易对同一个对象执行多次操作。

    this关键字对当前对象传递给其他方法也很有用。

    class Person { public void eat(Apple apple) { Apple peeled = apple.getPeeled(); System.out.println("Ymmy"); } } 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()); } }
    this关键字可以在构造器中调用构造器

    在构造器中为this增加了参数列表,这将产生对符合此参数列表的某个构造器的明确调用。

    import static net.mindview.util.Print.*; public class Flower { int petalCount = 0; String s = "initial value"; Flower(int petals) { petalCount = petals; print("Constructor petalCount = " + petalCount); } Flower(String ss) { print("Constructor String arg only, s = " + ss); s = ss; } Flower(String s, int petals) { this(petals); //this(s); this.s = s; print("String & int args"); } Flower() { this("hi", 47); print("default constructor (no args)"); } void printPetalCount() { // this(11); print("petalCount = " + petalCount + " s = " + s); } public static void main(String[] args) { Flower x = new Flower(); x.printPetalCount(); } }

    此处要注意构造器放在最开始的地方只能放在一个函数开头的第一行。

    this.xxx;xxx为变量代表的全局的数据成员。如果直接xxx就是局部的成员(当他们是同名的时候)

    printPetalCount() 指明了除了构造器以外,编译器进制在其他任何方法中调用构造器。

    static 含义

    static(静态)方法,static方法就是没有this的方法,在static内部不能调用非静态的方法,反之非静态方法可以调用静态方法。而且可以在没有创建对象的时候通过类来调用static方法。很像全局方法(java禁止全局方法)但是类中加入static方法就可以访问其他static方法和static域。。

    五、清理:终结和垃圾回收

    java中需要记住三点:

    1.对象可能不被垃圾回收

    2.垃圾回收不等于"析构"

    3.垃圾回收只与内存有关

    java中可以使用finalize()方法来进行一定的清理,但是不要过多使用。因为java本身就有垃圾回收机制,因此没有C++中的析构函数。但是垃圾回收器并不能完全替代析构函数(也不能直接调用finalize())如果希望进行释放存储空间之外的清理工作,还是必须明确调用某个恰当的java方法,等同于使用析构函数。

    记住:无论是“垃圾回收”还是“终结”,都不保证一定会发生,如果是Java虚拟机并未面临内存耗尽的情况,是不会浪费时间去执行垃圾回收去恢复内存的。

    不能指望finalize(),必须创建其他清理方法,并且明确调用。finalize()用处很小,但是有个有去写法。并不依赖每次对finalize()调用,就是对象终结条件的验证。

    当一个对象处于某种可以被清理的状态,内存可以安全释放时可以使用finalize()来验证这种状态是否存在。

    class Book { boolean checkedOut = false; Book(boolean checkOut) { checkedOut = checkOut; } void checkIn() { System.out.println("still exist"); checkedOut = false; } protected void finalize() { if(checkedOut) System.out.println("Error:checked out"); } } public class Termination { public static void main(String[] args) { Book novel = new Book(true); novel.checkIn(); //全局变量重新改为true new Book(true); //被回收了 System.gc(); //强行调用垃圾回收装置 novel.checkIn(); } }

    输出:

    still exist still exist Error:checked out

    事实上System.gc();只是提醒jvm需要进行垃圾回收 也是最后执行结束进行对finalize调用时才进行垃圾回收。

    垃圾回收技术

    (1)引用记数技术

    每个对象有一个引用计数器,有引用连接到对象的时候,引用计数器+1,当引用离开作用域或者被置位Null,引用记数-1.虽然开销不大 但是开销在整个生命周期都存在。垃圾回收器会在含有全部对象的的列表上遍历,发现引用记数为0,就释放空间。(引用计数模式会在记数为0时立刻释放)

    缺陷:如果对象之间存在循环引用,可能出现对象应该回收,但是引用计数不为0.

    实际上,引用计数多用来说明垃圾收集工作方式,事实上未被应用于JVM中。

    (2)自适应垃圾回收机制(停止-复制, stop-and-copy)

    先暂停程序的运行,将所有存活的对象从当前堆复制到另外一个堆,没有被复制的都是垃圾。当对象复制到新堆,一个挨着一个,所以新堆保持紧凑排序,再使用引用链条,对活的对象进行追溯在堆栈或静态存储区中的引用,其他就可以直接自动回收。解决了自引用问题。

    缺点:效率低需要维护两个堆的空间,Jvm中处理是按需从堆中分配几块较大的内存,复制动作在这些大块内存之间。还有一个问题是程序稳定后可能只有少量垃圾,但还是要从一处复制到另外一处,这里一些JVM会进行检查,没有新垃圾生成就会转到另外一种模式,体现了自适应)

    (3)标记-清扫(mark-and-sweep)

    同样从堆栈和静态存储区出发,遍历所有的引用,找出存活对象,找到一个存活的标记一下,这个过程不会回收对象。只有标记全部完成的时候,才开始清理动作。这个过程中没有标记的释放,但是不会有赋值动作。

    区别停止复制,这个是不会再后退进行的,垃圾回收动作发生的时候程序会暂停,标记-清扫也是程序暂停的时候才能进行。

    JVM会监视,对象稳定垃圾回收器小女帝就切换标记-清扫,如果标记清扫导致堆有很多碎片,就会切换会停止-复制变为连续方式。自适应技术。

    成员初始化

    java会尽力保证变量使用前都能恰当初始化

    如果是数据成员字段是基本类型,类的每一个基本数据类型都会保证有一个初始值。

    指定初始化

    指定为某个变量初始化:

    (1)定义类成员变量的地方赋值

    基本类型成员变量初始化

    public class InitialVal { char ch = 'x'; }

    非基本类型的对象也可以初始化 创建一个对象并且出事阿虎

    class Depth {} public class Measurement { Depth d = new Depth(); }

    (2)调用方法提高初值

    public class MethodInit { int i = f(); int f() { return 11; } }

    方法也可以带有参数 但是参数必须初始化

    public class MethodInit2 { int i = f(); int j = g(i); int f() { return 11;} int g(int n) { return n*10;} }

    构造器初始化

    构造器可以在运行时刻,调用方法来初始化。牢记:无法阻止自动初始化的进行,将在构造器被调用之前发生。

    public class Counter { int i; Counter() { i = 7; } }

    这样i会先为0 再变成7。构造器是会在新建对象的时候就会执行的代码段。

    初始化顺序

    类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散步于方法定义之间,仍旧会在任何方法(包括构造器)被调用之前得到初始化。变量会先初始化,然后才是调用方法。

    class Window { Window(int i) { System.out.println("Window " + i); } } class House { Window w1 = new Window(1); Window w2 = new Window(2); House() { System.out.println("I'm in house"); w3 = new Window(33); } Window w3 = new Window(3); public void f() { System.out.println("i'm in f()"); } } public class OrderOfInitial { public static void main(String[] args) { House h = new House(); h.f(); } } Window 1 Window 2 Window 3 I'm in house Window 33 i'm in f()

    输出可以知道变量会在调用构造器之前先初始化,w3在构造器内再次被初始化。

    w3会被初始化两次,构造器前与调用构造器(第一次对象被弃并且回收)。

    静态数据的初始化

    class Bowl { Bowl(int marker) { print("Bowl(" + marker + ")"); } void f1(int marker) { print("f1(" + marker + ")"); } } class Table { static Bowl bowl1 = new Bowl(1); Table() { print("Table()"); bowl2.f1(1); } void f2(int marker) { print("f2(" + marker + ")"); } static Bowl bowl2 = new Bowl(2); } class Cupboard { Bowl bowl3 = new Bowl(3); static Bowl bowl4 = new Bowl(4); Cupboard() { print("Cupboard()"); bowl4.f1(2); } void f3(int marker) { print("f3(" + marker + ")"); } static Bow bowl5 = new Bowl(5); } public class StaticInitalization { public static void main(String[] args) { print("Creating new Cupboard() in main"); new Cupboard(); print("Creating new Cupboard() in main"); new Cupboard(); table.f2(1); cupboard.f3(1); } static Table table = new Table(); static Cupboard cupboard = new Cupboard(); } 输出: Bowl(1) Bowl(2) Table() f1(1) Bowl(4) Bowl(5) Bowl(3) Cupboard() f1(2) Creating new Cupboard() in main Bowl(3) Cupboard() f1(2) Creating new Cupboard() in main Bowl(3) Cupboard() f1(2) f2(1) f3(1)

    注意此处,静态初始化,同样是在函数之前进行初始化,静态变量初始化会先于普通变量的初始化。初始化的顺序是先静态对象(如果他们没有被前面的对象创建过程而被初始化,如果静态对象被初始化了,他们只会被初始化一次,不会再次创建新的对象),再是非静态对象进行初始化(非静态对象创建新的对象就会重新new)。

    显式的静态初始化

    Java允许多个静态初始化组成成一个静态子句(静态块)

    public class Spoon { static int i; static { i = 47; } }

    长的像一个方法,但是实际上是一段在static关键词后面的代码。与其他静态初始化动作一样,这只执行一次:当首次生成这个类的一个对象,或者首次访问属于那个类的静态数据成员(即使从未生成过这个类的对象)栗子:

    package staticTestDemo; class Cup { Cup(int marker) { System.out.println("Cup(" + marker +")"); } void f(int marker) { System.out.println("f("+marker+")"); } } class Cups { static Cup cup1; static Cup cup2; static { cup1 = new Cup(1); cup2 = new Cup(2); } Cups() { System.out.println("Cups"); } } public class StaticTest { public static void mian(String[] args) { System.out.println("Test main"); Cups.cup1.f(1); } static Cups cups1 = new Cups(); static Cups cups2 = new Cups(); } Cup(1) Cup(2) f(1)

    无论通过静态声明与直接调用都会让Cups的静态初始化都会得到执行。并且只初始化一次。

    非静态实例初始化

    只有在调用的时候会进行初始化,初始的步骤也是非静态变量再构造函数,但是main函数会先被执行。故静态实例初始化>main函数>非静态初始化>构造函数

    数组初始化

    数组初始化

    int[] a1 = {1, 2, 3, 4, 5}; int[] a2; a2 = a1;

    这里a1 a2为数组的引用变量 实际上a2只是复制了a1的引用变量。

    import static net.mindview.util.Print.*; public class ArraysOfPrimitives { public static void main(String[] args) { int[] a1 = {1, 2, 3, 4}; int[] a2; a2 = a1; for(int i = 0; i < a2.length; i++) { a2[i] = a2[i] + 1; } for(int i = 0; i < a1.length; i++) { print("a1[" + i + "] = " a1[i]); } } } a1[0] = 2 a1[1] = 3 a1[2] = 4 a1[3] = 5 a1[4] = 6

    length()是字符串String的方法;用于求String的一种方法。

    也可以使用new来创造数组

    import java.util.*; import static net.mainview.util.Print.*; public class ArrayNew { public static void main(String[] args) { int[] a; Random rand = new Random(47); a = new int[rand.nextInt(20)]; print(a.length); print(Arrays.toString(a)); } } 5 [0, 0, 0, 0, 0]

    Random.nextInt()用于随机生成0到输入参数之间的值

    数组元素的基本数据类型值会初始化为空值,字符和数组为0,布尔型为false.

    Arrays.toString()方法属于Java.util标准库,将产生一维数组的可打印版本。

    如果创建了一个非基本类型的数组,就创建了一个引用数组,用整形包装器类Integer粒子:

    import java.util.*; import static net.mindview.util.Print.*; public class ArrayClassObj { public static void main(String[] args) { Random rand = new Random(47); Integer[] a = new Integer[rand.nextInt(20)]; print("length if a = " + a.length); for(int i = 0; i < a.length; i++) { a[i] = rand.nextInt(500); } print(Arrays.toString(a)); } }

    这里即使使用new创建数组

    Integer[] a = new Integer[rand.nextInt(20)];

    仍为一个引用数组,直到创建新的Integer对象,这里使用自动包装机产生的(rand.nextInt(500)),并且把新的对象赋值给引用。a[i] = rand.nextInt(500);

    如果忘记创建对象,尝试使用数组中的空引用值,就会产生异常。

    也可以用花括号括起来的列表来初始化对象数组:

    import java.util.*; public class ArrayInit { public static void main(String[] args) { Integer[] a = { new Integer(1), new Integer(2); 3, }; Integer[] b = new Integer[] { new Integer(1), new Integer(2), 3, }; System.out.println(Arrays.toString(a)); System.out.println(Arrays.toString(b)); } } [1, 2, 3] [1, 2, 3]

    这两种形式,初始化列表最后一个逗号可选。

    尽管第一种形式有用,但是只能用于数组被定义出。可以在任何地方使用第二种和第三种形式。

    甚至是方法调用的内部。

    public class DynamicArray { public static void main(String[] args) { Other.main(new String[] {"fiddle", "de", "dum"}); } } class Other { public static void main(String[] args) { for(String s : args) { System.out.print(s + " "); } } } fiddle de dum

    枚举类型enum

    enum关键字使得我们需要数组并且使用枚举类型即的时候就可以方便处理。

    public enum Spiciness { NOT, MILD, MEDIUM, HOTM FLAMING }

    为了使用enum需要创建一个该类型的引用:

    public class SimpleEnumUse { public static void main(String[] args) { Spiciness howHot = Spiciness.MEDIUM; System.out.println(howHot); } } 输出: MEDIUM

    创建enum的时候编译器会自动增加一些有用的特性,会创建toString()方法,方便显示某个enum实例的名字。

    编译器还会创建ordinal()方法,来表示某个特定enum常量的声明顺序,以及static values()方法。

    public class SimpleEnumUse { public static void main(String[] args) { for(Spiciness s : Spiciness.values()) System.out.println(s + ", ordinal " + s.ordinal()); } }

    enum有一个特别适用特性,可以在switch语句内使用:

    enum Spiciness { NOT, MILD, MEDIUM, HOT, FLANING; } public class Burrito { Spiciness degree; public Burrito(Spiciness degree) { this.degree = degree; } public void describe() { System.out.print("This burrito is "); switch(degree) { case NOT: System.out.println("not spicy at all."); break; case MILD: case MEDIUM: System.out.println("a little hot."); break; case HOT: case FLANING: default: System.out.println("maybe too hot."); } } public static void main(String[] args) { Burrito plain = new Burrito(Spiciness.NOT), greenChile = new Burrito(Spiciness.MEDIUM), jalapeno = new Burrito(Spiciness.HOT); plain.describe(); greenChile.describe(); jalapeno.describe(); } } This burrito is not spicy at all. This burrito is a little hot. This burrito is maybe too hot.

    switch要在有限的可能集合中选择,与enum是绝佳的组合。

    欢迎扫码加微信公众号交流!

    Processed: 0.043, SQL: 8