Java 中你可能没有注意到的一些事(持续更新中...)

本文深入解析Java中的垃圾回收机制,探讨静态与非静态变量的初始化过程,解释可变参数及方法重写的细节,分析组合与继承的区别,揭示内部类的工作原理,以及集合和异常处理的注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

垃圾回收

  1. 垃圾回收器只释放经由new分配的内存。
  2. 如果JVM并未面临内存耗尽的情形,他是不会浪费时间去执行垃圾回收的。

初始化

  1. 即使变量定义于方法体之间,仍旧会在任何方法(包括构造器)之前初始化。
  2. 静态变量会比非静态变量提前初始化,并且只初始化一次。
    (局部变量不会被初始化)

关于静态初始化

static标明的属性在类加载时就同时加载了,它会逐层寻找基类

什么意思呢?举个例子:有一个boy类,其父类为people
程序运行时会首先根据main所在类逐层向上找到people类中静态属性的声明,再逐层向下,找boy类中的静态属性,为其初始化。

代码如下:

class People {
    private int i = 9;
    People(){
        System.out.println("This is a people.");
    }
    private static int x1 = printInit("static People.x1 init.");
    static int printInit(String s) {
        System.out.println(s);
        return 47;
    }
}
public class Boy extends People {
    private int k = printInit("Boy.k init.");
    public Boy() {
        System.out.println("This is a Boy.");
    }
    private static int x2 = printInit("static Boy.x2 init.");
    public static void main(String[] args) {
        System.out.println("Boy constructor");
        Boy b = new Boy();
    }
}

我们可以看到输出结果为:

static People.x1 init.
static Boy.x2 init.
Boy constructor
This is a people.
Boy.k init.
This is a Boy.

就像我上面说的一样,先执行了顶层基类people中静态属性x1的初始化
而后执行其子类boy中静态属性x2的初始化
初始化之后,进入main方法
而在创建对象时,也是先执行其基类的构造方法,并初始化非静态属性后,再执行其构造方法

可变参数

  1. 可变参数传入格式:String... trailing , 不传也可以,但是要注意重载

方法的重写

  1. 尽量避免在构造器中调用其他方法(唯一安全的是调用private/final方法)

可能造成的混乱:如果在基类的构造其中调用重写的方法
访问的是由多态决定的子类的方法
属性因为还未构造,所以值为默认的0

同样用代码来举例:

class People {
    protected int i;
    People(){
        System.out.println("This is a people.");
        test();
    }

    protected void test() {
        System.out.println("This is People.i = " + i );
    }
}
public class Boy extends People {
    protected int i = 10;
    public Boy() {
        System.out.println("This is a Boy.");
        test();
    }

    @Override
    protected void test() {
        System.out.println("This is Boy.i = " + i );
    }

    public static void main(String[] args) {
        System.out.println("Boy constructor");
        Boy b = new Boy();
    }
}

可以看到,在子类Boy中重写了test()方法
那么如果在基类People的构造器中调用test(),会是什么结果呢?

Boy constructor
This is a people.
This is Boy.i = 0
This is a Boy.
This is Boy.i = 10

出人意料的,我们发现当我们是声明一个Boy对象时,会执行其父类People的构造器
而在People构造器中执行的test()方法,却是子类Boy中重写的方法
这是由于多态的原因,具体原因这里不赘述,我会单开一章来详细说明多态的实现原理。
而我们发现,即使调用的是子类重写的方法,其打印出的属性却是因还未构造所以默认为0的属性。
这就会为程序带来不可知的错误,所以应尽量避免这种情况的发生。

  1. 重写的方法可以返回子类。
    对于重写的方法来说,要求其入参和返回参数一致,但返回参数也可以是其子类,代码如下:
class People {
    protected int i;
    People(){
        System.out.println("This is a people.");
    }

    protected People test() {
        System.out.println("This is People test()");
        return new People();
    }
}
public class Boy extends People {
    protected int i = 10;
    public Boy() {
        System.out.println("This is a Boy.");
    }

    @Override
    protected Boy test() {
        System.out.println("This is Boy test()");
        return new Boy();
    }

    public static void main(String[] args) {
        System.out.println("Boy constructor");

        Boy b = new Boy().test();
    }
}

我们在子类中重写了test()方法,并且返回的是People的子类,一样可以执行。

输出结果:

This is a people.
This is a Boy.
This is Boy test()
This is a people.
This is a Boy.

组合优于继承

为什么会这么说?组合到底比继承优秀在了哪里?

其实并不能说“优秀”,而应该是“各司其职”
通常,我们用继承来区分不同的行为
而组合的优点在于其可以在类中灵活应用绑定不同对象
我们用代码来说明组合的用处:

class People {
    public void people(){}
}

class Boy extends People {
    @Override
    public void people() {
        System.out.println("This is a boy.");
    }
}

class Girl extends  People {
    @Override
    public void people() {
        System.out.println("This is a girl.");
    }
}

class Stage {
    private People people = new Boy();

    public void change() {
        people = new Girl();
    }

    public void performPlay() {
        people.people();
    }
}

public class Test {
    public static void main(String[] args) {
        Stage stage = new Stage();
        stage.performPlay();
        stage.change();
        stage.performPlay();
    }
}

输出结果:

This is a boy.
This is a girl.

可以看到,在Stage类中声明了一个People,将其组合起来,我们就能在Stage中随意改变其引用
比如一开始people的引用指向的为boy,操作其行为也是boy中重写的方法
但是当我们想引发其他行为的时候,就可以用change()boy转型为girl,因为其所属基类一致
此时,再执行行为就是girl的行为了,而这是继承不能达成的效果,因为不能在运行期间继承不同的对象

继承决定行为间的差异,而组合表达状态的变化。

继承接口可以获得新的接口

但要注意的一点是,尽量避免去组合多个接口
比如接口A中有方法f(),接口B中同样有方法f()
同时实现A与B就会造成混乱

内部类

  1. 在匿名内部类使用的参数应设定为final
  2. 内部类无法被覆盖
  3. 使用内部类最吸引人的原因是:每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响

集合

看下面一段代码:

List<Integer> ls = Arrays.asList(1, 2, 3);

ls.add(4);

这段代码会报错:Exception in thread "main" java.lang.UnsupportedOperationException
原因是:Arrays.asList(T... a)返回的是Arrays的内部类ArrayList,而不是util包下的ArrayList
后者重写了removeadd等方法而前者没有重写,所以会抛出异常。

异常

对于子类覆写的父类的方法而言,只能声明父类方法声明的异常或不声明异常,比如:

class AA {
    public void testA() throws NullPointerException {}
}

class C extends AA {
    @Override
    public void testA() throws Exception {}
}

这样写是错误的,因为父类的方法中只抛出了NullPointerException
所以子类如果抛出异常,只能抛出父类声明的异常,当然不抛出异常也是可以的。

可以在子类方法中尝试捕捉其他异常:

class AA {
    public void testA() throws NullPointerException {}
}

class C extends AA {
    @Override
    public void testA() {
        try {
            int[] is = new int[1];
            System.out.println(is[1]);
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
        }
    }
}

而对于父类方法中未声明异常列表,子类覆写的方法可以随意抛出异常:

class AA {
    public void testA() {}
}

class C extends AA {
    @Override
    public void testA() throws NullPointerException {}
}

这样写同样是可行的。

静态内部类

注意静态内部类是不随着外部类加载而加载的,只有当访问静态内部类中的变量或者方法时才会进行加载,其加载时会先加载外部类。

关于asList方法

要注意的是,当将基本类型的数组,比如整型数组int[]Arrays.asList方法进行转化时,数组是作为一个整体被传入list之中的,也就是说list的大小为1.
(String不属于基本类型所以不受限制)

Java8提供了一种stream方法可以将基本类型数组(只支持int、long、double)转为List:

List<Integer> aList = Arrays.stream(testList).boxed().collect(Collectors.toList());

(.boxed方法是一个中间方法,可以将每个元素包装为一个Integer作为stream返回)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值