跳至主要內容

②JAVA 语言特性

holic-x...大约 11 分钟JAVA基础

②JAVA 语言特性

学习核心

  • 参数传递

    • 形参和实参的区别是什么?
    • Java是值传递还是引用传递?
    • 值传递和引用传递的区别是什么?
  • final关键字

    • final作用是什么?
    • final、finally、finalize有什么不同
  • static关键字

    • static作用是什么?
    • static和final的区别是什么?

参数传递

Java中将实参传递给方法(或函数)的方式是值传递

  • 如果参数是基本类型:传递的是基本类型的值拷贝(会创建副本)
  • 如果参数是引用类型:传递的是引用对象在堆中地址值的拷贝(内存地址),同样也会创建副本

1.形参和实参

​ 形参:形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的

​ 实参:实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的

​ Java调用的过程中,形参的作用域在方法内部,将实参传递给形参

public class ParamDemo {

    // 此处的方法定义中的参数列表为String name(name为形式参数)
    private static void show(String name){
        System.out.println(name);
    }

    public static void main(String[] args) {
        String name = "noob"; // 此处的name为实际参数
        show(name);// 将实际参数传入方法
        System.out.println(name);
    }

}

2.值传递和引用传递

值传递: 是指在调用方法时,将实际参数拷贝一份传递给方法,这样在方法中修改形式参数时,不会影响到实际参数。

引用传递: 地址传递,是指在调用方法时,将实际参数的地址传递给方法,这样在方法中对形式参数的修改,将影响到实际参数。

值传递:传递的是内容副本引用传递:传递的是实际内存地址副本

Java为什么只有值传递?

1)基本类型参数
/**
 * 基本数据类型
 */
public class BasicTypeDemo {

    public static void update(int count){
        // 修改count的值
        count ++ ;
        System.out.println("update count:"+count);
    }

    public static void main(String[] args) {
        int count = 0;
        update(count);
        System.out.println("main count:"+count);
    }
}

// 输出结果
update count:1
main count:0

​ 基于输出结果分析,可以看到update中的数值修改只是改变了形参的count值,并没有对main方法中的实参count值进行改变。结合Java调用去理解:Java基本数据类型是存储在虚拟机栈内存中,栈中存放着栈帧,方法调用的过程,就是栈帧在栈中入栈、出栈的过程

image-20240520151807033

2)引用类型参数(按值传递:传的是地址)

数组类型的参数传递

// 传递引用类型参数案例1:数组类型
class ArrayDemo{
    public static void change(int[] array){
        // 将数组中的第一个元素变为0
        array[0] = 0;
    }

    public static void main(String[] args) {
        // 定义int类型数组
        int[] arr = {1,2,3,4,5};
        System.out.println("调用前:" + arr[0]);
        // 调用change方法
        change(arr);
        // 打印数组信息
        System.out.println("调用后:" + arr[0]);
    }
}

// 结果输出
调用前:1
调用后:0

此处不要误解Java对引用类型的参数采用的是引用传递,实际上此处传递的还是值(只不过这个值是实参的地址)

​ change方法的参数拷贝的是arr(实参)的的地址,它和array(形参)指向的是同一个数组对象,因此也说明方法内部对形参的修改会影响到实参

Java对象引用类型

​ 为了进一步佐证上述arr的场景案例,此处引入Java对象验证Java的“按值传递”

// 传递引用类型参数案例2:Java对象
class User{
    private String name;
    // 构造函数定义
    public User(String name) {
        this.name = name;
    }
    public static void swap(User ua,User ub){
        // 定义一个中间变量用于交换两者User对象
        User tUser = ua;
        ua = ub;
        ub = tUser;
        System.out.println("swap userA:" + ua.name);
        System.out.println("swap userB:" + ub.name);
    }

    public static void main(String[] args) {
        User userA = new User("小A");
        User userB = new User("小B");
        // 交换对象
        swap(userA,userB);
        System.out.println("main userA:" + userA.name);
        System.out.println("main userB:" + userB.name);
    }
}

// 测试结果
swap userA:B
swap userB:A
main userA:A
main userB:B

​ 基于上述测试结果:两个引用类型的形参互换,但是并没有影响到实参的内容。

​ swap方法的参数ua、ub只是拷贝的实参userA、userB的地址,因此ua、ub的互换只是拷贝的两个地址的互换,并不会影响到实际userA、userB的地址

基于上述案例则可进一步说明,无论是基本数据类型还是引用数据类型,都是按值传递

​ 当传递基本数据类型,比如原始类型(int、long、char等)、包装类型(Integer、Long、String等),实参和形参都是存储在不同的栈帧内,修改形参的栈帧数据,不会影响实参的数据。

​ 当传参的引用类型,形参和实参指向同一个地址的时候,修改形参地址的内容,会影响到实参。当形参和实参指向不同的地址的时候,修改形参地址的内容,并不会影响到实参

​ 什么是引用传递:参考C++的指针(引用传递),对形参的修改就是对实参的修改

Java为什么不引入引用传递?

​ 引用传递:能在方法内把实参值修改了,但是Java为什么不引入引用传递呢?

参考

​ 出于安全考虑,方法内部对值进行的操作,对于调用者都是未知的(把方法定义为接口,调用方不关心具体实现)。你也想象一下,如果拿着银行卡去取钱,取的是 100,扣的是 200,是不是很可怕。

​ Java 之父 James Gosling 在设计之初就看到了 C、C++ 的许多弊端,所以才想着去设计一门新的语言 Java。在他设计 Java 的时候就遵循了简单易用的原则,摒弃了许多开发者一不留意就会造成问题的“特性”,语言本身的东西少了,开发者要学习的东西也少了

final关键字

final、finally、finalize

final

​ final:可以用来修饰类、方法、变量;

  • final修饰的类不可被继承
  • final修饰的方法不可被重写(override)
  • final修饰的变量不可被修改

finally 异常机制

​ finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作

finalize 垃圾回收机制

​ finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为 deprecated

扩展:finalize的利弊

​ finalize 的执行是和垃圾收集关联在一起的,一旦实现了非空的 finalize 方法,就会导致相应对象回收呈现数量级上的变慢,有人专门做过 benchmark,大概是 40~50 倍的下降。

​ 因为,finalize 被设计成在对象被垃圾收集前调用,这就意味着实现了 finalize 方法的对象是个“特殊公民”,JVM 要对它进行额外处理。finalize 本质上成为了快速回收的阻碍者,可能导致对象经过多个垃圾收集周期才能被回收。

​ 是否可以考虑用 System.runFinalization() 告诉 JVM 积极一点来解决这个问题?也许有点用,但是问题在于,这还是不可预测、不能保证的,所以本质上还是不能指望。实践中,因为 finalize 拖慢垃圾收集,导致大量对象堆积,也是一种典型的导致 OOM 的原因。

​ 从另一个角度,要确保回收资源就是因为资源都是有限的,垃圾收集时间的不可预测,可能会极大加剧资源占用。这意味着对于消耗非常高频的资源,千万不要指望 finalize 去承担资源释放的主要职责,最多让 finalize 作为最后的“守门员”,况且它已经暴露了如此多的问题。这也是为什么我推荐,资源用完即显式释放,或者利用资源池来尽量重用

​ finalize 还会掩盖资源回收时的出错信息,截取自 java.lang.ref.Finalizer(参考JDK源码)

 private void runFinalizer(JavaLangAccess jla) {
 //  ... 省略部分代码
 try {
    Object finalizee = this.get(); 
    if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
       jla.invokeFinalize(finalizee);
       // Clear stack slot containing this variable, to decrease
       // the chances of false retention with a conservative GC
       finalizee = null;
    }
  } catch (Throwable x) { }
    super.clear(); 
 }

​ 这段代码存在问题:此处的Throwable 是被生吞了的!也就意味着一旦出现异常或者出错,得不到任何有效信息。况且,Java 在 finalize 阶段也没有好的方式处理任何信息,不然更加不可预测。

  • 有什么机制可以替换 finalize 吗?

​ Java 平台目前在逐步使用 java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。Cleaner 的实现利用了幻象引用(PhantomReference),这是一种常见的所谓 post-mortem 清理机制。利用幻象引用和引用队列,可以保证对象被彻底销毁前做一些类似资源回收的工作,比如关闭文件描述符(操作系统有限的资源),它比 finalize 更加轻量、更加可靠。吸取了 finalize 里的教训,每个 Cleaner 的操作都是独立的,它有自己的运行线程,所以可以避免意外死锁等问题。

​ 实践中,可以为自己的模块构建一个 Cleaner,然后实现相应的清理逻辑。下面是 JDK 自身提供的样例程序:

public class CleaningExample implements AutoCloseable {
        // A cleaner, preferably one shared within a library
        private static final Cleaner cleaner = <cleaner>;
        static class State implements Runnable { 
            State(...) {
                // initialize State needed for cleaning action
            }
            public void run() {
                // cleanup action accessing State, executed at most once
            }
        }
        private final State;
        private final Cleaner.Cleanable cleanable
        public CleaningExample() {
            this.state = new State(...);
            this.cleanable = cleaner.register(this, state);
        }
        public void close() {
            cleanable.clean();
        }
    }

​ 注意,从可预测性的角度来判断,Cleaner 或者幻象引用改善的程度仍然是有限的,如果由于种种原因导致幻象引用堆积,同样会出现问题。所以,Cleaner 适合作为一种最后的保证手段,而不是完全依赖 Cleaner 进行资源回收,不然就要再做一遍 finalize 的噩梦。

​ 很多第三方库自己直接利用幻象引用定制资源收集,比如广泛使用的 MySQL JDBC driver 之一的 mysql-connector-j,就利用了幻象引用机制。幻象引用也可以进行类似链条式依赖关系的动作,比如,进行总量控制的场景,保证只有连接被关闭,相应资源被回收,连接池才能创建新的连接。

​ 另外,这种代码如果稍有不慎添加了对资源的强引用关系,就会导致循环引用关系,前面提到的 MySQL JDBC 就在特定模式下有这种问题,导致内存泄漏。上面的示例代码中,将 State 定义为 static,就是为了避免普通的内部类隐含着对外部对象的强引用,因为那样会使外部对象无法进入幻象可达的状态。

static关键字

static关键字基本概念

​ 基本概念:被static关键字修饰的不需要创建对象去调用,直接根据类名就可以去访问

​ java中static一般用来修饰成员变量或函数。但有一种特殊用法是用static修饰内部类,普通类是不允许声明为静态的

1.static修饰内部类

class StaticClass{
    // static关键字修饰被不累
    public static class InnerClass{
        // 内部类构造方法定义
        InnerClass(){
            System.out.println("静态内部类");
        }
        // 内部类方法
        public void InnerMethod(){
            System.out.println("静态内部方法");
        }
    }

    public static void main(String[] args) {
        // 通过StaticClass类名访问静态内部类
        InnerClass innerClass = new StaticClass.InnerClass();
        // 静态内部类(和普通类一样使用)
        innerClass.InnerMethod();
    }
}

2.static修饰方法

// static修饰方法
class StaticMethod{
    // 定义静态方法
    public static void show(){
        System.out.println("show");
    }

    public static void main(String[] args) {
        // 方式1:通过类名直接访问静态方法
        StaticMethod.show();

        // 方式2:通过对象访问静态方法
        StaticMethod staticMethod = new StaticMethod();
        staticMethod.show();
    }
}

3.static修饰变量

// static修饰变量
class StaticVariable{
    private static String name = "noob";
    public static void main(String[] args) {
        System.out.println(StaticVariable.name);
    }
}

4.static修饰代码块

// static修饰代码块(结合继承机制分析)
class Father{
    // 父类静态代码块
    static{
        System.out.println("father static");
    }
    // 父类构造方法
    public Father(){
        System.out.println("father constructor");
    }
}
class Son extends Father{
    // 子类静态代码块
    static{
        System.out.println("son static");
    }
    // 子类构造方法
    public Son(){
        System.out.println("son constructor");
    }
}

class StaticCodeBlock{
    public static void main(String[] args) {
        Son son = new Son();
    }
}

// 执行结果
father static
son static
father constructor
son constructor
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3