Java之面向对象(下)







final修饰符

final修饰的啥,啥就不能变。



final修饰成员变量

final修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果没有显示的给该变量赋值,那么系统将会帮忙初始化一下。如果没有初始值,那么后续只能对该成员变量进行一次赋值,不能进行多次的赋值。

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

final修饰成员变量的两种情况:

  • 类变量:可以在静态初始化块和声明该类变量时指定初始值。
  • 实例变量:可在非静态初始化块、声明该实例变量和构造器中指定初始值。

例如:

public class FinInstance {


    final static int num = 1;

    // 实例变量的值,在初始化代码块中进行初始化
    final String name;

    final int age;
    {
        // 下面这行代码会报错,因为age还没有被初始化,Java允许使用方法来对该变量进行访问
        // System.out.println(age);
        name="name变量在初始化代码块中进行赋值";
        printAge(); // 访问没有初始化的age变量
        age = 18;
    }

    public void printAge() {
        System.out.println("还未初始化的age变量 = "+age);
    }


    public static void main(String[] args) {
        System.out.println("num = "+FinInstance.num);
        new FinInstance();
    }


}

从上面的程序看出,使用方法访问final成员变量,系统将final变量默认初始化为0(null,false等)。


注:非静态不能使用静态。不推荐,在没有初始化final成员变量之前使用方法访问这个变量(这样子做显然是是没有意义的)。



final修饰局部变量

由于系统不会对局部变量进行初始化,所以局部变量必须由程序员显式初始化。因此使用final修饰局部变量时,既可以在定义的时指定默认值,也可以不指定默认值(实参的赋值)。

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

例如:

public class FinLocal {

    public static void test(final int num) {

        // 不能对final修饰的形参赋值,下面语法非法
        // num = 10;
        System.out.println("num = "+num);
    }
    

    public static void main(String[] args) {
        test(8);
        final String name = "张三";
        // 下面语法非法
        // name = "李四";
        
        final int age;
        age = 18;
        
        // 下面语法非法
        // age = 19;
    }


}



final修饰基本类型和引用类型的区别

当使用final修饰基本类型时,后续不能再对其进行更改。当使用final修饰引用类型时,只能保证一直引用的是同一个对象,即一直指向的是同一个地址,但可以修改这个对象的状态。

例如:

class Perosn {
    private String name;

    public Perosn(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

public class FinDifference {

    public static void main(String[] args) {
        // final修饰基本数据类型
        // num的数值不能被改变
        final int num = 0;
        System.out.println("num = " + num);

        // final修饰引用数据类型
        final Perosn p = new Perosn("张三");
        System.out.println("p.name = " + p.getName());
        p.setName("李四");
        System.out.println("p.name = " + p.getName());
    }
}



final修饰方法

final修饰的方法不可以被重写,如果父类的某些方法不想让子类进行重写,给这些方法加上该关键字即可。

例如:

class Super {
    // 派生类将无法重写该方法
    public final void test(){};
    // 虽然不可以被重写,但可以对方法进行重载
    public final void test(int num){};
}

class Sub extends Super{
    // 下面语句将会导致编译错误 
    // public final void test(){};
}

来继续看下面这个例子,我们将权限修改符进行修改,并在子类中创建出一模一样的,如下:

class Super {
    private final void test(){};
}

class Sub extends Super{
    public final void test(){};
}

可见,当权限是private时,该不可变方法只能是父类自己可见,那么子类自己声明一个一模一样的是完全没有问题的。迷惑性的重写父类方法,这只是定义了和父类一模一样的方法,并不是重写父类的方法。



final修饰类

简单一句话:final修饰的类不能被继承。

例如:

final class A {}
// 下面的类定义将会出现编译错误
public class FinClass extends A{}




抽象类

概念

当某个父类只是知道其子类应该包含怎么样的方法,但无法准确地知道这些子类如何实现这些方法,这时就应将父类定义成抽象的。抽象方法和抽象类必须使用abstract修饰符来定义,其中抽象方法是只能有方法签名,没有方法体的方法。抽象方法充当着站位的角色 ,它们具体实现在子类中。

抽象类和抽象方法的规则,如下:

  • 抽象类和抽象方法必须使用abstract关键字来修饰,其中抽象方法,不能含有方法体。
  • 抽象类不能被实例化。
  • 抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接口、枚举类)。构造器可以提供给子类使用。

例如:

public abstract class A {
    public abstract void test();
}

其中,抽象方法和空方法体区别:

抽象方法语法格式:

public abstract void test();

空方法体语法格式:

public void test(){}

很明显可以看出,抽象方法它连有方法体都没有,空方法体方法只是方法体为空。



使用

扩展(继承)抽象类的两种注意事项:

  • 如果子类不重写父类中的全部抽象方法,那么就必须把该子类定义为抽象类。
  • 重写父类中的全部抽象方法。

例如:

// 将类B定义为抽象类
public abstract class B extends A{
}

// 重写类A中的所有抽象方法
public class C extends A{
    @Override
    public void test() {
        System.out.println("重写A类的抽象方法");
    }
}

注:有抽象方法的类一定是抽象类,抽象类不一定有抽象方法。


小案例:

abstract class Shape {
    private String shapeName;

    public Shape(String shapeName) {
        this.shapeName = shapeName;
    }

    public Shape() {
    }

    public String getShapeName() {
        return shapeName;
    }

    public abstract void calPerimeter();

}

class Triangle extends Shape {

    private double a;
    private double b;
    private double c;

    public Triangle(){}

    public Triangle(String shapeName, double a, double b, double c) {
        super(shapeName);
        this.a = a;
        this.b = b;
        this.c = c;
    }


    @Override
    public void calPerimeter() {
        if (!(a + b <= c || a + c <= b || b + c <= a)) {
            System.out.println(getShapeName() + "周长是:" + (a + b + c));
        } else {
            System.out.println("该图形不是三角形");
        }
    }
}


public class AbTest {

    public static void main(String[] args) {
        new Triangle("三角形",3,4,5).calPerimeter();
    }

}

注:抽象类体现的就是第一种模板设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造。




接口

概念

接口(interface)是比抽象类还要抽象的特殊的“抽象类”。接口这种技术主要用来描述类具体有什么功能,而并不是给出每个功能的具体实现。一个类可以实现一个或多个接口,而一个类最多只能继承一个父类。

接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式定义。

接口的使用规则:

  • 权限修饰符可以是public或者是省略,省略时系统默认接口的访问权限是包权限。
  • 一个接口可以有多个直接父接口,单接口只能单继承接口,不能继承类。

因为接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里面可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(接口、枚举)定义。


当接口中定义的不是默认方法、类方法或私有方法,不管你有没有显式使用public abstract修饰符,接口里的普通方法总是能使用public abstract来修饰。接口里的普通方法不能有方法体;但类方法、默认方法、私有方法都必须有方法体。


如定义一个接口:

public interface Test {
}

注:java8以上的版本中可以在接口中定义默认方法、类方法,Java9为了接口增加了一种私有方法,私有方法中可以提供方法体。



使用

一个类实现一个接口,需要下面两个步骤:

  1. 将类声明为实现给定的接口。
  2. 对接口中的所有方法进行实现。

接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。子接口扩展某个父接口时,将会获得父接口里面定义的所有抽象方法、常量。

例如:

interface A {
    // 系统自动增加public static final修饰符
    int numA = 1;
    default void info() {
        System.out.println("这是A接口的默认方法");
    }

    public void testA();
}

interface B {
    int numB = 2;
    default void info() {
        System.out.println("这是B接口的默认方法");
    }

    public void testB();
}


interface C extends A, B {
    int numC = 3;
    @Override
    default void info() {
        System.out.println("C接口重写A,B接口的默认方法");
    }

    @Override
    default void testA() {
        System.out.println("C接口重写A接口的testA方法");
    }

    @Override
    default void testB() {
        System.out.println("C接口重写B接口的testB方法");
    }
}



public class Test implements C {


    public static void main(String[] args) {

        Test test = new Test();
        test.info();
        test.testA();
        test.testB();
        System.out.println("C.numA = "+C.numA+"\nC.numB = "+C.numB+"\nC.numB = "+C.numC);

    }

}



接口和抽象类之间的区别

对于没有实战经验的博主来说,只能看到的是接口的出现弥补了Java单继承的不足。




枚举类

概念

在JDK1.5之前,要实现枚举类,需要手动定义类用静态常量来实现枚举(代码量比较大)。在JDK1.5之后,新增加了对枚举类的支持,新增关键字enum(它与class、interface关键的平级),用于定义枚举类。还有对switch的控制表达式也进行了扩展,switch的控制表达式可以是任何枚举类型。当switch控制表达式使用枚举类型时,后面case表达式中的值直接使用枚举值得名字,不需要添加枚举类作为限定。

枚举类与普通类之间的区别:

  • 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是默认继承Object类,因此枚举类不能显示继承其他父类。
  • 使用enum定义、非抽象的枚举类会默认使用final修饰,因此枚举类不能派生子类。
  • 枚举类的构造器只能使用private权限修饰符(不管隐式还是显式)。
  • 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例(系统会自动给实例添加pulic static final 修饰符)。

创建枚举类,例如:

public enum Season {
    // 系统会自动添加 public static fianl 修饰符
    SPRING,SUMMER,AUTUMN,WINTER;
}

class SeasonTest {

    private static void choose(Season s) {

        switch (s) {
            case SPRING:
                System.out.println("春天");
                break;

            case SUMMER:
                System.out.println("夏天");
                break;

            case AUTUMN:
                System.out.println("秋天");
                break;

            case WINTER:
                System.out.println("冬天");
                break;

            default:
                break;
        }
    }


    public static void main(String[] args) {
        // 类成员,类直接调用
        System.out.println(Season.SPRING);

        System.out.println();

        // 调用values方法,遍历所有枚举值
        Season[] values = Season.values();
        for (Season value : values) {
            System.out.println(value);
        }

        System.out.println();

        choose(Season.SPRING);

    }
}



枚举类的成员变量、方法、构造器

枚举类本质是类,它也是具有类的一些共性的。例如:

public enum Gender {
    // 调用有参构造器
    MALE("男"),FEMALE("女");

    private final String name;

    // 枚举类的构造器只能使用private修饰符,因为枚举类不能派生子类
    Gender(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

从上面程序可以看出,在枚举类列出枚举值时,实际上就是调用构造器创建类对象,只是这里省略了new关键字,也没有显示调用构造器。上面代码中的枚举值等价于下面两行代码:

public static final Gender MALE = new Gender("男");
public static final Gender FEMALE = new Gender("女");



枚举类实现接口

枚举类也可以实现一个或多个接口,跟普通类实现接口一样,也需要实现该接口中所包含的方法。例如:

interface SeasonDesc {
    void info();
}

public enum Season implements SeasonDesc{
    // 系统会自动添加 public static fianl 修饰符
    SPRING,SUMMER,AUTUMN,WINTER;

    @Override
    public void info() {
        System.out.println("这是一个定义季节的枚举类");
    }
}

看上面程序,这里会出现一个问题:如果由枚举类来实现接口里的方法,则每个枚举值在调用该方法时都具有相同的行为方式(因为共用一个方法)。如果需要每个枚举值在调用方法时呈现出来不一样的行为方式,则可以让每个枚举值分别来实现方法(提供自己的方法体),即可实现让不同的枚举值调用方法时具有不同的行为方式。例如:

interface SeasonDesc {
    void info();
}

public enum Season implements SeasonDesc{
    // 系统会自动添加 public static fianl 修饰符
    SPRING(){
        public void info() {
            System.out.println("该枚举值代表春天");
        }
    },
    SUMMER(){
        public void info() {
            System.out.println("该枚举值代表夏天");
        }
    },
    AUTUMN(){
        public void info() {
            System.out.println("该枚举值代表秋天");
        }
    },
    WINTER(){
        public void info() {
            System.out.println("该枚举值代表冬天");
        }
    };

}

从上面代码可以看出,这种语法跟内部类的语法很像。当创建枚举值时,并不是直接创建枚举类的实例,而是相当于创建枚举类匿名子类的实例。前面有提到过枚举类不能派生子类(因为被关键字final修饰),那么在这又是什么情况?额,并不是所有的枚举类都是使用了final修饰,注意上面规则中有说到非抽象的枚举类才默认使用final修饰。对于一个抽象的枚举类而言,只要它包含了抽象方法,它就是抽象枚举类,系统会默认使用abstract来修饰,不是使用final关键字。


注:编译上面程序,从生成的三个文件中可以看出,SPRING等实际上是Season匿名子类的实例,而不是Season类的实例(这就说明,它继承了接口中的抽象方法,然后由匿名子类去实现的,所以可以派生)。



枚举类包含抽象方法

跟上面接口逻辑一样。例如:

public enum Season {
    // 系统会自动添加 public static fianl 修饰符
    SPRING(){
        public void info() {
            System.out.println("该枚举值代表春天");
        }
    },
    SUMMER(){
        public void info() {
            System.out.println("该枚举值代表夏天");
        }
    },
    AUTUMN(){
        public void info() {
            System.out.println("该枚举值代表秋天");
        }
    },
    WINTER(){
        public void info() {
            System.out.println("该枚举值代表冬天");
        }
    };
    
    // 无需给类添加abstract关键字
    public abstract void info();


}

注:因为枚举类需要显示创建枚举值,而不是作为父类,所以定义每个枚举值时必须要为抽象方法提供实现,否则将出现编译错误。




内部类

把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为内部类,包含内部类的类也被称为外部类。内部类的主要作用,如下:

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

内部类与外部类定义的语法,有下面区别:

  • 内部类比外部类可以多使用三个修饰符:private、protected、static。外部类只是能包访问权限和public。
  • 非静态内部类不能拥有静态成员。

以下这种方法不是内部类:

同一个Java源文件里定义了多个类,它们依然是两个相互独立的类。

例如:

class A{}
public class B{}

注:上面的程序,在编译后会生成两个类的字节码文件。



非静态内部类

将一个类放在这个类的内部,内部类就定义完成。

例如:

public class Test1 {


    private int num = 0;


    // 定义内部类
    private class InTest1 {

        // 定义内部类的成员变量
        // 只是演示,不创建getter和setter方法
        private int num = 1;

        public InTest1() {
        }

        private void info() {
            System.out.println("内部类中的num值:"+num);
        }


    }


    public void test() {
        InTest1 inTest1 = new InTest1();
        inTest1.info();
    }


    public static void main(String[] args) {
        new Test1().test();
    }


}

注:编译上面的程序,看到在文件所在的路径生成了两个class文件,一个是Test1.class,另一个是Test1$InTest1.class,前者是外部类的字节码文件,后者是内部类的字节码文件。


看上面的程序时,你是否会有这种疑问,为什么不直接在外部类的main方法中创建内部类的对象呢?

非静态内部类,必须有一个外部类的引用才能创建(非静态内部类对象依赖于外部类对象)。在外部类的非静态方法中,都隐式的存在着外部类的对象引用(this),所以可以直接创建非静态内部类对象。在外部类静态方法中(类方法),隐式存在外部类的类引用,并没有外部类的对象引用,所以会出错。总之,不允许在外部类的静态成员中直接使用非静态内部类。

那么可以这么整:

    public static void main(String[] args) {
        new Test1().new InTest1().info();
    }

当内部类要访问外部类中的同名域时,系统查询的顺序是局部、内部、外部。如果显式的访问同名域,可以通过使用this、外部类类名.this作为区分。例如:

        private void info() {
            int num = 2;
            System.out.println("Test1.this.num的值:"+Test1.this.num);
            System.out.println("InTest1.this.num的值:" + InTest1.this.num);
            System.out.println("InTest1.this.info.num的值:"+num);

        }

非静态内部类的成员可以访问外部类的private成员,反之报错。非静态内部类的成员只能在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问非静态内部类的成员,则必须显示创建非静态内部类对象来调用访问其成员变量。



静态内部类

使用static修饰的内部类,与其说是静态内部类,不如说是属于外部类本身的,而不是属于外部类的某个对象的。

静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态成员内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。

例如:

public class Test2 {


    private static int num = 0;


    // 定义内部类
    static class InTest2 {

        // 定义内部类的成员变量
        // 只是演示,不创建getter和setter方法
        private static int  num = 1;
        private boolean flag = true;

        public InTest2() {
        }

        private static void info() {
            int num = 2;
            System.out.println("Test2.num的值:"+Test2.num);
            System.out.println("InTest2.num的值:" + InTest2.num);
            System.out.println("info中num的值:"+num);
        }


    }
    

    public static void main(String[] args) {
        Test2.InTest2.info();
        System.out.println("静态内部类的对象可以在main函数中直接创建:"+new InTest2().flag);
    }


}

静态内部类是外部类的一个静态成员,因此外部类的所有方法、初始化块中可以使用静态内部类来定义变量、创建对象等。

外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。

例如:InTest1.num;



局部内部类

如果把一个内部类放在方法里面定义,则这个内部类就是一个局部内部类,局部内部类仅在该放法内有效。由于局部内部类不能再外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符。

例如:

public class Test3 {


    public static void main(String[] args) {

		// 定义一个局部内部类
        class InTest3 {
            private int num = 0;

        }

        System.out.println(new InTest3().num);

    }

}

编译上面程序,看到生成了两个class文件:Test3.class,Test3$1InTest3.class。局部内部类的class的文件名比成员内部类的class的文件名多了一个数字,这个数字是用来区分同一个类中同名的局部内部类。


注:局部内部类的作用范围仅限于它所在的方法范围,出了这个范围之后就获取不到内部类中的成员变量。



匿名内部类

匿名内部类跟匿名对象一样,只适合在用一次的场景下使用。

匿名内部类定义格式是:

new 实现接口() | 父类构造器(实参列表)
{
	// 匿名内部类的实现细节
}

匿名内部类使用规则:

  • 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。
  • 匿名内部类不能定义构造器。由于匿名内部类没有类名(没有class关键字修饰),所以不能定义构造器。匿名内部类中可以使用初始化代码块。

例如:

public class AnnoymousInnerClass {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 9; i++) {
                    System.out.println(Thread.currentThread().getName()+" : "+i);
                }
            }
        }).start();
    }
}

注:定义匿名内部类无需class关键字,而是定义匿名内部类时直接生成该匿名内部类的对象。




对象和垃圾回收

当程序创建对象、数组等引用类型实体时系统都会在堆内存中为之分配一块内存区,对象就保存在这块内存区中,当这块内存不再被任何引用变量所引用时,这块内存就变成垃圾,等待垃圾回收机制进行回收。

垃圾回收具有的特征:

  • 垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(例如数据库连接、网络IO等资源)。
  • 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。
  • 在垃圾回收机制回收任何对象之前,总会先调用它的finalize()方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收。



对象在内存中的状态

当一个对象在堆内存运行时,根据它被引用变量所引用的状态,可以分为以下三种状态:

  • 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。
  • 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的finalize()方法进行资源清理。如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变成可达状态;否则该对象将进入不可达状态。
  • 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize()方法后依然没有使该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。

示意图:

1


下面程序简单演示一下上面的流程:

public class ObjStatus {
    
    public static void test() {
        String s = new String("A");
        s = new String("B");
    }
    
    public static void main(String[] args) {
        test();
    }
    
}

在上面程序中,主函数调用test方法,方法体中定义了一个s变量,并让这个变量指向"A"字符串,该代码运行结束后,该字符串属于可达状态。执行下一行时,代码让s变量重新指向了"B"字符串,该代码运行结束后,字符串"B"为可达状态,字符串"A"变为可恢复状态。


注:当某个对象被其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态;当某个类对象被其他对象的实例变量引用时,只有当该对象被销毁后,该对象才会进入可恢复状态。



强制垃圾回收

程序无法精确控制Java垃圾回收的时机,所以有时候需要进行强制性的垃圾处理。所谓的强制回收垃圾就是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。


强制垃圾回收的两种方式:

  • System类中的静态方法gc。
  • Runtime对象的gc实例方法。

系统看心情回收,程序如下:

public class MandatoryRecycling {

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new MandatoryRecycling();
        }
    }

    public void finalize() {
        System.out.println("清理垃圾……");
    }

}

运行结果:

2

从运行结果中可以看出,系统并没有去回收该资源。


强制(通知系统)回收,程序如下:

public class MandatoryRecycling {
    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new MandatoryRecycling();
            System.gc();
            // 作用一样
            // Runtime.getRuntime().gc();
        }
    }

    public void finalize() {
        System.out.println("清理垃圾……");
    }

}

运行结果:

3

从运行结果看出,系统在接到垃圾回收的通知后,会尽快的进行垃圾回收。



finalize方法

在垃圾回收机制回收某个对象所占用的内存之前,通常要求程序调用适当的方法来清理资源,在设有明确清理资源的情况下,Java提供了默认机制来清理该对象的资源,这个机制就是finalize()方法。该方法是定义在Object类里的实例方法

protected void finalize() throws Throwable

finalize()方法具有如下4个特点:

  • 永远不要主动调用某个对象的finalize()方法,该方法应交给垃圾回收机制调用。
  • finalize()方法何时被调用,是否被调用具有不确定性,不要把finalize()方法当成一定会被执行的方法。
  • 当JVM执行可恢复对象的fianlize()方法时,可能使该对象或系统中其他对象重新编程可达状态。
  • 当JVM执行finalize()方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。

下面程序演示在finalize方法中复活:

public class FinalizeTest {
    private static FinalizeTest ft = null;

    public void info() {
        System.out.println("测试finalize方法");
    }

    public void finalize() {
        // 指向将要被销毁的引用,使其重新变成可达状态
        ft = this;
    }

    public static void main(String[] args) {
        new FinalizeTest();
        System.gc();
        // 强制调用finalize方法
        System.runFinalization();
        ft.info();
    }
}

运行上面的程序,你会发现ft依然存在。



对象的软、弱和虚引用

注:这部分将在后续填充。




参考资料:

Java核心技术卷1基础知识

疯狂Java讲义



如上内容有误,还劳烦各位大佬赐教,本人会及时改正。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值