②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基本数据类型是存储在虚拟机栈内存中,栈中存放着栈帧,方法调用的过程,就是栈帧在栈中入栈、出栈的过程
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