2023年Java核心技术面试第二篇(篇篇万字精讲)

简介: 2023年Java核心技术面试第二篇(篇篇万字精讲)

四. 强引用,软引用,弱引用,幻象引用之间的区别?



1.前言


Java语言中,除了原始数据类型的变量,其他都是引用类型,指向各种不同的对象,理解引用可以帮助掌握Java对象生命周期和JVM内部相关机制


2.强引用


不同引用类型主要体现的是对象不同的可达性(reachable)状态对垃圾收集的影响


强引用("Strong" Reference),即是我们常见的普通对象引用,只要存在强引用指向一个对象,就表明对象还"活着",垃圾收集器不会碰这种对象,对于一个普通的对象,如果没有其他引用关系,只要进行超过了引用的作用域或者将强引用赋值为null,就可以被垃圾回收器收集。


解析内容:


2.1 强引用赋值为null


假设我们有一个Java类"Person"


代码如下:

public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}



现在我们创建一个强引用指向该对象:

Person person = new Person("John");


此时,对象"John"拥有一个强引用person,因此垃圾收集器不会回收该对象。

如果我们将强引用置为null:

person = null;


此时,没有任何强引用指向对象"John",它成为了垃圾对象,可以被垃圾收集器回收。具体的回收时机会根据垃圾收集策略来确定。


2.2 超过了引用的作用域


2.2.1 描述:

当引用超出其作用域时,意味着该引用无法被访问到。在Java中,一个对象的作用域通常是定义它的代码块或方法。


2.2.2 作用域内:

public class ScopeExample {
    public static void main(String[] args) {
        Person person; // 在作用域外声明引用
        {
            person = new Person("John");
            System.out.println(person.getName()); // 输出:John
        }
        // 在作用域外仍然可以使用person引用
        System.out.println(person.getName());
    }
}


2.2.3  不在作用域内:

public class ScopeExample {
    public static void main(String[] args) {
        {
            Person person = new Person("John");
            System.out.println(person.getName()); // 输出:John
        }
        // 此处无法访问person引用,超出了其作用域
        // System.out.println(person.getName()); 
    }
}


3. 软引用(SoftReference)


3.1 描述


相对于强引用引用弱化一些的引用,可以让对象进行一些垃圾收集,只有当JVM认为内存不足的时候,会尝试进行回收软引用指向的对象。


JVM会确保在抛出OutOfMemoryError之前,进行清理软引用指向的对象。


软引用通常用来实现内存敏感的缓存,如果还有空间内存,可以暂时保留缓存,当内存不足的时候清理掉,保证了使用缓存的同时,不会耗尽内存。


4. 弱引用(WeakReference)


不能使对象豁免垃圾收集,提供一种访问在弱引用状态下对象的途径,构建一种没有特点约束关系,维护一种非维护的映射关系,如果试图获取对象还在,就使用它,否则重现实例化。


4.1 解析:


定义了一个ExpensiveObject类,表示一个耗费资源的对象。通过定义get_object函数和cache字典作为缓存,我们可以使用弱引用来构建非强制性的映射关系。当需要获取对象时,首先检查缓存中是否有对应的弱引用对象,如果存在且未被垃圾回收,则直接返回该对象;否则,重新实例化对象,并将其添加到缓存中。


需要注意的是,由于弱引用并不能阻止对象被垃圾回收,当对象被垃圾回收后,弱引用将返回None。因此,在使用弱引用构建缓存时,需要在适当的时机进行缓存项的更新和清理,以保证缓存数据的正确性和有效性。


import java.lang.ref.WeakReference;
class ExpensiveObject {
    private String name;
    public ExpensiveObject(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
public class WeakReferenceExample {
    public static void main(String[] args) {
        // 缓存示例
        WeakReference<ExpensiveObject> cache = new WeakReference<>(null);
        ExpensiveObject obj1 = getOrCreateObject("Example");
        System.out.println(obj1.getName());  // 输出: Example
        ExpensiveObject obj2 = getOrCreateObject("Example");
        System.out.println(obj2.getName());  // 输出: Example
        // 因为缓存中已经存在名为"Example"的对象,第二次获取时直接从缓存中获取,并不会重复创建
    }
    public static ExpensiveObject getOrCreateObject(String name) {
        ExpensiveObject obj = null;
        // 从缓存中获取弱引用对象
        WeakReference<ExpensiveObject> objRef = cache.get();
        if (objRef != null) {
            obj = objRef.get();
        }
        if (obj == null) {
            // 如果缓存中没有对应的对象或对象已经被垃圾回收了,重新创建并缓存
            obj = new ExpensiveObject(name);
            cache = new WeakReference<>(obj);
        }
        return obj;
    }
}


5. 幻像引用


5.1 描述


也叫虚引用,不能通过他访问对象,幻像引用提供了一种确保对象被finalize后,做某些事情的机制,可以用幻象引用监控对象的创建和销毁


5.2 流程图

a8badcb173e4415b854468defef9948f.png

 强引用

6c9389b55a6b468691d62eea5c08b241.png


5.3 解析:


5.3.1 引用队列(ReferenceQueue)的作用主要体现在以下两个方面:


1.监控对象的销毁:通过创建ReferenceQueue<TrackedObject>类型的引用队列referenceQueue,并将幻象引用对象注册到引用队列中:


phantomReference = new PhantomReference<>(this, referenceQueue);

当TrackedObject对象被垃圾回收器标记为可回收时,对应的幻象引用就会被放入引用队列中。通过检查引用队列中是否存在幻象引用,我们可以得知对象已经被finalize,并且即将被回收。


2.统计对象的创建和销毁次数:ObjectTracker类中的trackObjectCreation()和trackObjectDestruction()方法用于跟踪对象的创建和销毁次数。在TrackedObject的构造方法中,当对象被创建时,会调用ObjectTracker.trackObjectCreation()方法增加创建次数。而在finalize方法中,会调用destroy()方法,进而调用ObjectTracker.trackObjectDestruction()方法增加销毁次数。


通过结合引用队列、幻象引用和ObjectTracker类,我们可以在程序中监控对象的创建和销毁情况,并获取相应的统计数据。在示例中,通过输出ObjectTracker.getCreatedCount()和ObjectTracker.getDestroyedCount(),我们可以分别得到对象的创建次数和销毁次数。


引用队列在该示例中的主要用途是监控对象的销毁并统计对象的创建和销毁次数。


通过引用队列和幻象引用的配合使用,我们可以实现更精确的对象跟踪和资源管理。


循环来检查引用队列中是否有幻象引用。通过调用referenceQueue.poll()方法,我们可以从引用队列中获取幻象引用。如果获取到了幻象引用,则意味着对象已经被finalize,并且即将被回收


5.3 .2 监控对象的销毁并统计对象的创建和销毁次数


import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
class ObjectTracker {
    private static int createdCount = 0;
    private static int destroyedCount = 0;
    public static void trackObjectCreation() {
        createdCount++;
    }
    public static void trackObjectDestruction() {
        destroyedCount++;
    }
    public static int getCreatedCount() {
        return createdCount;
    }
    public static int getDestroyedCount() {
        return destroyedCount;
    }
}
class TrackedObject {
    private String name;
    private PhantomReference<TrackedObject> phantomReference;
    public TrackedObject(String name, ReferenceQueue<TrackedObject> referenceQueue) {
        this.name = name;
        phantomReference = new PhantomReference<>(this, referenceQueue);
        ObjectTracker.trackObjectCreation();
    }
    public void destroy() {
        ObjectTracker.trackObjectDestruction();
    }
}
public class PhantomReferenceMonitoringExample {
    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<TrackedObject> referenceQueue = new ReferenceQueue<>();
        // 创建对象并跟踪创建次数
        TrackedObject obj1 = new TrackedObject("Object 1", referenceQueue);
        TrackedObject obj2 = new TrackedObject("Object 2", referenceQueue);
        System.out.println("Created count: " + ObjectTracker.getCreatedCount());  // 输出: Created count: 2
        // 销毁对象并跟踪销毁次数
        obj1.destroy();
        obj2.destroy();
        // 清理引用队列,判断是否存在幻象引用
        PhantomReference<?> phantomRef;
        while ((phantomRef = (PhantomReference<?>) referenceQueue.poll()) != null) {
            System.out.println("Object finalized and ready for destruction: " + phantomRef);
        }
        System.out.println("Destroyed count: " + ObjectTracker.getDestroyedCount());  // 输出: Destroyed count: 2
    }
}


5.3.3 对象清理:


定义了一个ExpensiveObject类,其中重写了finalize方法,在对象被垃圾回收前会执行该方法。通过创建一个幻象引用PhantomReference并指定相应的引用队列ReferenceQueue,我们可以在程序中检查对象是否已经被 finalize。


在主函数中,模拟进行垃圾回收操作,并等待一段时间以确保垃圾回收完成。然后从引用队列中尝试获取幻象引用所引用的对象,如果对象已经被 finalize,则可以通过引用队列获取到幻象引用,否则对象仍然存在。


幻象引用通常与引用队列结合使用,以便在对象被 finalize 后执行特定的操作

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
class ExpensiveObject {
    private String name;
    public ExpensiveObject(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("Object " + name + " is finalized.");
    }
}
public class PhantomReferenceExample {
    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<ExpensiveObject> referenceQueue = new ReferenceQueue<>();
        PhantomReference<ExpensiveObject> phantomReference = new PhantomReference<>(new ExpensiveObject("Example"), referenceQueue);
        // 模拟垃圾回收动作
        System.gc();
        Thread.sleep(1000);  // 等待垃圾回收完成
        // 检查是否已经被finalize
        if (referenceQueue.poll() != null) {
            System.out.println("Object has been finalized.");
        } else {
            System.out.println("Object still exists.");
        }
    }
}


5.4 流程图解析:


5.4.1 强可达(Strongly Reachable)


就是当一个对象可以有一个或多个线程可以不通过各种

引用访问到的情况。比如,我们新创建一个对象,那么创建它的线程对它就是强可达。


5.4.2 软可达(Softly Reachable)


就是当我们只能通过软引用才能访问到对象的状态。

5.4.3 弱可达(Weakly Reachable)


类似前面提到的,就是无法通过强引用或者软引用访问,

只能通过弱引用访问时的状态。这是十分临近 finalize 状态的时机,当弱引用被清除的时

候,就符合 finalize 的条件了。


5.4.4幻象可达(Phantom Reachable)


上面流程图已经很直观了,就是没有强、软、弱引用

关联,并且 finalize 过了,只有幻象引用指向这个对象的时候。


5.4.5 最后的状态


就是不可达(unreachable),意味着对象可以被清除了。


除了幻象引用(因为 get 永远返回 null),如果对象还没有被销毁,都可以通过 get 方法获

取原有对象。这意味着,利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,

也就是人为的改变了对象的可达性状态


五. String,StringBuffer,StringBuilder之间的区别



1.String


String是Java语言非常重要的类,提供了构造和管理字符串的各种逻辑。

典型的lmmutable类,被声明成final class,所有属性也都是final,由于不可变性,类似拼接,剪裁字符串等动作,都会产生新的String对象。

由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明细的影响。


1.1 解析:


1.1.1lmmutable类:


Immutable 类是指在对象创建后,其状态无法被修改的类。换句话说,Immutable 类的实例一旦创建就不能被改变。对于 String 类来说,它被设计为 Immutable 类,意味着一旦创建了一个 String 对象,其中包含的字符序列就不能被改变。


1.1.2 好处:


  1. 线程安全:由于 String 类是不可变的,多线程环境下可以保证对象内容的不变性,从而避免了同步操作。
  2. 缓存利用:由于字符串常量是不可变的,因此可以被缓存起来,提高程序的性能和效率。
  3. 安全性:String 对象作为方法参数时,不会被修改,保证了数据的安全性。


1.1.3 注意:


虽然 String 对象本身是不可变的,但是通过反射等手段可以绕过限制,直接修改对象的内部状态。所以在涉及到安全性要求较高的场景中,还是需要额外的措施来保证对象的不可变性。


1.1.4 字符串操作对应用性能有影响的原因主要包括以下几个方面:


1.1.4.1 内存开销:

由于 String 类是不可变的,每次对字符串进行拼接、剪裁等操作都会创建一个新的字符串对象。这就导致了频繁的内存分配和释放,增加了垃圾回收的压力,消耗了额外的内存空间。


1.1.4.2字符串拷贝:

在字符串拼接或者剪裁时,通常涉及到将多个字符串合并为一个新的字符串,或者从原字符串中截取一部分形成新的字符串。这些操作都需要将原始字符串的内容复制到新的字符串对象中,如果字符串长度较大或者操作频繁,将会产生大量的内存拷贝操作,降低了性能。


1.1.4.3 时间复杂度:

某些字符串操作的时间复杂度较高,例如字符串拼接操作的时间复杂度为 O(n^2),其中 n 为字符串的长度。当需要拼接大量的字符串时,操作的时间复杂度会呈现出指数级增长,导致性能下降


2. StringBuffer


解决拼接产生太多中间对象的问题而提供的一个类,可以使用append或者add方法,将字符串添加到已有序列的末尾或者指定位置。

Stringbuffer本质是一个线程安全的可修改字符序列,保证了线程安全,但是也带来了额外的性能开销,所以除非有线程安全的需要,不然还是采用StringBuilder


3. StringBuilder


Java1.5新增加的,在能力上面和StringBuffer没有本质区别,去掉了线程安全的部分,有效的减小了开销,绝大部分情况下字符串拼接的首选。


4. 考点分析:


应用开发离不开操作字符串,理解字符串的设计和实现以及相关工具,拼接类的使用,对于写出高质量代码很有帮助。


5.  总结:


5.1 String:


5.1.1 String的创建机理

由于String在Java世界中使用过于频繁,Java为了避免在一个系统中产生大量的String对象,

引入了字符串常量池。


5.1.2 其运行机制是:

创建一个字符串时,首先检查池中是否有值相同的字符串对象,如果有则不需要创建直接从池中刚查找到的对象引用;如果没有则新建字符串对象,返回对象引用,并且将新创建的对象放入池中。但是,通过new方法创建的String对象是不检查字符串池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入池中。


上述原则只适用于通过直接量给String对象引用赋值的情况。


5.1.3 举例:

String str1 = "123"; //通过直接量赋值方式,放入字符串常量池

String str2 = new String(“123”);//通过new方式赋值方式,不放入字符串常量池

注意:String提供了inter()方法。调用该方法时,如果常量池中包括了一个等于此String对象

的字符串(由equals方法确定),则返回池中的字符串。否则,将此String对象添加到池中,

并且返回此池中对象的引用


5.1.4 String的特性


5.1.4.1 不可变

指String对象一旦生成,则不能再对它进行改变。不可变的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅度提

高系统性能。不可变模式是一个可以提高多线程程序的性能,降低多线程程序复杂度的设计

模式。


5.1.4.2针对常量池的优化

当2个String对象拥有相同的值时,他们只引用常量池中的同一个拷

贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间


5.2 StringBuffer/StringBuilder


StringBuffer和StringBuilder都实现了AbstractStringBuilder抽象类,拥有几乎一致对外提供的

调用接口。

其底层在内存中的存储方式与String相同,都是以一个有序的字符序列(char类型

的数组)进行存储,不同点是StringBuffer/StringBuilder对象的值是可以改变的,并且值改变

以后,对象引用不会发生改变;两者对象在构造过程中,首先按照默认大小申请一个字符数组,由于会不断加入新数据,当超过默认大小后,会创建一个更大的数组,并将原先的数组

内容复制过来,再丢弃旧的数组。因此,对于较大对象的扩容会涉及大量的内存复制操作,

如果能够预先评估大小,可提升性能。

唯一需要注意的是:StringBuffer是线程安全的,但是StringBuilder是线程不安全的。

StringBuffer类中方法定义前面都会有synchronize关键字。

因此,StringBuffer的性能要远低于StringBuilder。


5.3  应用场景


在字符串内容不经常发生变化的业务场景优先使用String类。

例如:常量声明、少量的字符串拼接操作等。如果有大量的字符串内容拼接,避免使用String与String之间的“+”操作,因为这样会产生大量无用的中间对象,耗费空间且执行效率低下(新建对象、回收对象花费大量时间)

在频繁进行字符串的运算(如拼接、替换、删除等)并且运行在多线程环境下


5.3.1 使用StringBuffer,例如HTTP参数解析与封装


5.3.1.1 多线程环境下使用同步的 StringBuffer 进行字符串拼接的例子


创建了两个线程 thread1 和 thread2 分别向 StringBuffer 对象 stringBuffer 中追加字符 "A" 和 "B",每个线程追加1000次。由于 StringBuffer 是线程安全的,我们使用 synchronized 关键字对 stringBuffer 进行同步处理,确保每个线程在访问和修改 stringBuffer 时的互斥性。


通过启动两个线程并等待它们执行完成,最后输出 stringBuffer 的长度,可以观察到最终拼接的字符串长度为 2000,证明在多线程环境下使用同步的 StringBuffer 进行字符串拼接是安全和可靠的。


public class ThreadSafeStringBufferExample {
    public static void main(String[] args) {
        final StringBuffer stringBuffer = new StringBuffer();
        // 创建两个线程进行字符串拼接
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (stringBuffer) {
                    stringBuffer.append("A");
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (stringBuffer) {
                    stringBuffer.append("B");
                }
            }
        });
        // 启动线程
        thread1.start();
        thread2.start();
        // 等待线程执行完成
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 输出最终字符串长度
        System.out.println("Final string length: " + stringBuffer.length());
    }
}


在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程环境下

建议使用StringBuilder,例如SQL语句拼装、JSON封装等。


SQL语句拼装和JSON封装,建议使用非线程安全的 StringBuilder 类来进行字符串拼接操作。StringBuilder 的性能比线程安全的 StringBuffer 更好,因为它不需要进行同步处理。


5.3.2 使用 StringBuilder 进行SQL语句拼装和JSON封装的示例:


5.3.2.1 SQL拼接


创建了一个 StringBuilder 对象 sql,并使用连续的 append() 方法将 SQL 语句的各个部分拼接起来,包括表名、列名和列值。最后,通过调用 toString() 方法将 StringBuilder 转换为字符串并输出。

public class SQLStatementExample {
    public static void main(String[] args) {
        StringBuilder sql = new StringBuilder();
        String tableName = "users";
        String columnName = "name";
        String columnValue = "John Doe";
        sql.append("SELECT * FROM ")
           .append(tableName)
           .append(" WHERE ")
           .append(columnName)
           .append(" = '")
           .append(columnValue)
           .append("'");
        String query = sql.toString();
        System.out.println(query);
    }
}


输出:

SELECT * FROM users WHERE name = 'John Doe'


使用 StringBuilder 进行 SQL 语句的动态拼装。通过使用非线程安全的 StringBuilder,可以避免多余的同步开销,提高字符串拼接的效率和性能。


5.3.2.2 JSON封装


创建了一个 StringBuilder 对象 json,用于存储 JSON 数据。然后,使用 JSONObject 和 JSONArray 类来构建 JSON 结构,并将其转换为字符串形式。最后,通过调用 toString() 方法将 StringBuilder 转换为字符串并输出。


import org.json.JSONArray;
import org.json.JSONObject;
public class JSONExample {
    public static void main(String[] args) {
        StringBuilder json = new StringBuilder();
        // 创建一个 JSONObject 对象并设置键值对
        JSONObject person = new JSONObject();
        person.put("name", "John Doe");
        person.put("age", 30);
        person.put("isStudent", false);
        // 创建一个 JSONArray 对象并添加元素
        JSONArray hobbies = new JSONArray();
        hobbies.put("reading");
        hobbies.put("coding");
        hobbies.put("gaming");
        // 将 JSONArray 添加到 JSONObject 中
        person.put("hobbies", hobbies);
        // 将 JSONObject 转换为字符串
        json.append(person.toString());
        String jsonString = json.toString();
        System.out.println(jsonString);
    }
}


输出:

{"name":"John Doe","age":30,"isStudent":false,"hobbies":["reading","coding","gaming"]}


使用 StringBuilder 进行 JSON 数据的封装。通过使用非线程安全的 StringBuilder,可以避免多余的同步开销,提高字符串拼接的效率和性能。


请注意,在多线程环境下进行字符串拼接操作时,如果存在并发访问和修改的情况,建议使用线程安全的 StringBuffer 或采用适当的同步机制来保证数据的一致性和线程安全性。


相关文章
|
2天前
|
Java
Java面向对象实践小结(含面试题)(下)
Java面向对象实践小结(含面试题)(下)
10 1
|
2天前
|
设计模式 Java
Java面向对象实践小结(含面试题)(上)
Java面向对象实践小结(含面试题)
12 1
|
4天前
|
JavaScript 前端开发 Java
【JAVA面试题】什么是引用传递?什么是值传递?
【JAVA面试题】什么是引用传递?什么是值传递?
|
6天前
|
存储 安全 Java
[Java基础面试题] Map 接口相关
[Java基础面试题] Map 接口相关
|
6天前
|
Java
[Java 面试题] ArrayList篇
[Java 面试题] ArrayList篇
|
6天前
|
SQL Java 数据库连接
Java从入门到精通:3.1.2深入学习Java EE技术——Hibernate与MyBatis等ORM框架的掌握
Java从入门到精通:3.1.2深入学习Java EE技术——Hibernate与MyBatis等ORM框架的掌握
|
6天前
|
SQL Java 数据库连接
Java从入门到精通:2.3.1数据库编程——学习JDBC技术,掌握Java与数据库的交互
ava从入门到精通:2.3.1数据库编程——学习JDBC技术,掌握Java与数据库的交互
|
6天前
|
设计模式 存储 前端开发
Java从入门到精通:2.2.1学习Java Web开发,了解Servlet和JSP技术,掌握MVC设计模式
Java从入门到精通:2.2.1学习Java Web开发,了解Servlet和JSP技术,掌握MVC设计模式
|
6天前
|
Java API
Java从入门到精通:2.1.5深入学习Java核心技术之文件操作
Java从入门到精通:2.1.5深入学习Java核心技术之文件操作
|
2月前
|
Java 程序员
java线程池讲解面试
java线程池讲解面试
62 1