Java基础与面试(二)

1.WeakReference vs SoftReference vs PhantomReference vs Strong reference 的区别

在垃圾回收工作的环境中,引用类尤其重要。我们都知道,垃圾回收器会回收那些符合垃圾收集条件的对象的内存,但是没有多少程序员知道这个资格是基于指向对象的引用的。这也是Java中WeakReference 和SoftReference之间的主要区别。如果只有弱引用指向该对象,并且它们被急切地收集,那么垃圾回收器会回收了这个对象。另一方面,当JVM需要内存时,会回收具有SoftReference引用的对象。
SoftReference和WeakReference的这些特殊行为使它们在某些情况下很有用,例如,SoftReference在实现缓存时看起来非常完美,所以当JVM需要内存时,它会移除那些只有SoftReference 指向的对象。
另一方面,弱引用对于存储元数据非常有用,比如,存储ClassLoader对象。如果没有类被加载,那么就没有必要保存对ClassLoader的引用,WeakReference 会使ClassLoader在最后一个强引用被删除时就有资格进行垃圾收集。

WeakReference vs SoftReference

Java有四种引用:
1.Strong reference
2.Weak Reference
3.Soft Reference
4.Phantom Reference
强引用是最简单的,因为我们在日常编程中使用它。
如下:String s = “abc”,引用变量s对字符串对象“abc”有很强的引用。
任何具有强引用的对象都不适合进行垃圾收集。显然,这些是Java程序所需要的对象。弱引用用java.lang.ref.WeakReference表示,您可以使用以下代码创建弱引用:

Counter counter = new Counter(); // 强引用 
WeakReference<Counter> weakCounter = new WeakReference<Counter>(counter)  //弱引用
counter = null; // 现在counte对象有资格回收

现在,当您创建的强引用count = null时,第1行创建的counter对象就有资格进行垃圾回收;因为它没有任何强引用,而它所有的弱引用不能阻止count的回收。另一方面,如果这是一个软引用的话,Counter对象就不会被垃圾收集,直到JVM完全需要内存时才会回收它。Java中的软引用使用 java.lang.ref.SoftReference 表示。您可以使用以下代码在Java中创建一个SoftReference。

Counter prime = new Counter();//prime持有强引用
SoftReference<Counter> soft = new SoftReference<Counter>(prime) ; //soft是一个对于Counter 对象的软引用
prime = null; //现在,Counter对象有资格进行垃圾收集,但只有在JVM绝对需要内存时才会收集。

在将创建的强引用prime 置为 null后,Counter对象只有一个在第2行创建的软引用,它不能阻止垃圾收集,但是它可以延迟收集,而在WeakReference的情况下,垃圾回收则是迫切的。由于SoftReference和WeakReference之间的这个主要差异,SoftReference更适合于缓存,WeakReference更适合存储元数据。WeakReference的一个方便的例子是WeakHashMap,它是Map接口的另一个实现,但是与HashMap的实现不同,它具有更特殊的特性,WeakHashMap将键包装为弱引用,这意味着一旦对实际对象的强引用都被移除了的话,在WeakHashMap内部出现的弱引用并不能阻止它们被垃圾收集。
虚引用(Phantom reference )是java.lang.ref 包中可用的第三种引用类型。虚引用由java.lang.ref.PhantomReference 表示。当一个对象只有一个虚引用时,那么每当垃圾回收期愿意的时候就可以回收它。它与SoftReference以及WeakReference比较类似,下面是使用虚引用的代码:

DigitalCounter digit = new DigitalCounter();//强引用的digit 
PhantomReference<DigitalCounter> phantom = new PhantomReference<DigitalCounter>(digit); //创建对于digit对象到的虚引用
digit = null;

一旦删除了强引用,在第3行创建的DigitalCounter对象在任何时候都可以被垃圾收集,因为它只有一个指向它的虚引用,这不能阻止它被GC。

喜欢的可以研究一下ReferenceQueue

2. Java编程语言中的JRE JVM和JDK的区别。

Java运行时环境(JRE)

在浏览器、移动、电视或机顶盒中,Java无处不在,如果您喜欢Java编程语言,那么您就会知道Java代码是在JAR (Java archive)文件中绑定的,需要Java虚拟机JVM来执行它。现在,JVM是一个可执行的或程序,就像任何其他程序一样,您可以将其安装到您的机器中。您已经看到浏览器经常建议下载JRE来运行从Internet下载的Java Applet。可以使用java.oracle.com中的各种版本的JRE,大多数用户只想在浏览器中执行Java程序或独立下载JRE。所有浏览器,包括ie, Firefox和Chrome都可以使用JRE。

Java虚拟机(JVM)

当您下载JRE并在您的机器上安装时,您的机器就得到了创建JVM所需的所有代码。Java虚拟机是在使用Java命令运行Java程序时创建的(如Java HelloWorld)。JVM负责将字节代码转换为特定于机器的代码,这就是为什么Windows、Linux或Solaris都有不同的JVM,但是一个JAR可以在所有操作系统上运行。Java虚拟机是Java编程语言的核心,它为Java程序员提供了一些特性,包括内存管理和垃圾收集、安全性和其他系统级服务。可以定制Java虚拟机。在JVM创建时,我们可以指定JVM中堆大小的起始内存或最大内存。如果我们向java命令提供了无效的参数,它可能会拒绝创建java虚拟机,并告诉我们“failed to create Java virtual machine: invalid argument”。简而言之,Java虚拟机或JVM是为Java提供平台独立性的人。

Java开发工具包(JDK)

JDK也被统称为JRE,但是它比JRE多得多,它提供了编译调试和执行Java程序所需的所有工具和可执行文件。和JRE一样,JDK也是特定于平台的,您需要使用单独的安装程序在Linux和Windows上安装JDK。安装JDK时,安装文件夹通常被称为JAVA_HOME。所有二进制文件都位于JAVA_HOME/bin中,其中包括javac、java和其他二进制文件,它们必须位于系统路径中,以便编译和执行java程序。

JRE、JDK和JVM之间的差异

简而言之,以下是JRE、JDK和JVM之间的一些差异:
1)JRE和JDK以安装程序的形式出现,而JVM则与它们捆绑在一起。
2)JRE只包含执行java程序的环境,但不包含编译java程序的其他工具
3)JVM与JDK和JRE一起出现,当您通过提供“Java”命令执行Java程序时创建

即时编译(JIT)

最初Java被指责性能不佳,因为它同时编译和解释指令。因为编译或Java文件到类文件是独立于Java程序的执行的,所以不要混淆。编译指的是将字节码转变为机器码。JIT是Java虚拟机的高级部分,它通过同时编译相似的字节码来优化字节码到机器指令转换部分,从而减少了整个执行时间。JIT是Java虚拟机的一部分,它还执行一些其他的优化,比如内联函数。

3. Java的堆

Java中的堆空间是什么?

当Java程序启动时,Java虚拟机从操作系统获得一些内存。Java虚拟机或JVM使用此内存来满足其所有需要,而该内存的一部分称为Java堆内存。堆在Java中,一般位于地址空间底部,向上移动。无论何时我们使用new操作符创建对象,或任何其他方法,都意味着对象从堆中分配内存,当对象死或垃圾收集时,内存就会返回到Java堆空间

Java堆和垃圾收集

我们知道对象是在堆内存中创建的,而垃圾收集是一个过程,它从Java堆空间中移除死对象,并将内存返回到Java堆中。为了将垃圾收集堆划分为新生代、老年代、永久代(Java已经移除了该代,改用元数据区)三个主要区域。
Java堆的新生代是Java堆内存的一部分,一个新创建的对象存储在该区,在应用程序过程中,许多对象创建并死亡,当经历了Major GC或者Full GC后仍然存活的对象则可能被移动到老年代或者永久代。Java堆的永久代是JVM存储类和方法、字符串池和类级细节的元数据的地方。

4. Java 中堆和栈有什么区别?

这里写图片描述

在Java中堆栈与堆的区别

1)堆和堆栈的主要区别是,堆栈内存用于存储局部变量和函数调用,而堆内存用于存储Java中的对象。无论在代码中创建对象(如成员变量、局部变量或类变量),它们都是在Java中的堆空间中创建的。
2)Java中的每个线程都有自己的堆栈,可以使用-Xss JVM参数指定,同样,您也可以使用JVM选项-Xms和-Xmx指定Java程序的堆大小,其中-Xms的初始大小是堆的大小,-Xmx是Java堆的最大大小。
3)如果堆栈中没有存储函数调用或局部变量的内存,JVM将抛出java.lang。StackOverFlowError,如果没有更多的堆空间来创建对象,JVM将抛出java.lang。OutOfMemoryError:Java。
4)如果您正在使用递归,即调用自身,那么可能很快就会将堆栈区填满。栈和堆之间的另一个区别是,栈内存的大小比Java中堆内存的大小要小得多。
5)存储在堆栈中的变量只对所有者线程可见,而在堆中创建的对象对所有线程都可见。换句话说,堆栈内存是Java线程的一种私有内存,而堆内存是在所有线程之间共享的。

5.“a==b”和”a.equals(b)”有什么区别?

如果 a 和 b 都是对象,则 a==b 是比较两个对象的引用,只有当 a 和 b 指向的是堆中的同一个对象才会返回 true,而 a.equals(b) 是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写 equals() 方法,所以可以用于两个不同对象,但是包含的字母相同的比较。

6. final、finalize 和 finally 的不同之处?

final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。
finalize 方法是在对象被回收之前调用的方法,给对象自己最后一个复活的机会,但是什么时候调用 finalize 没有保证。
finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。

7. Java 中的编译期常量是什么?

公共静态不可变(public static final )变量也就是我们所说的编译期常量,这里的 public 可选的。
实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变。这种方式存在的一个问题是你使用了一个内部的或第三方库中的公有编译时常量,但是这个值后面被其他人改变了,但是你的客户端仍然在使用老的值,甚至你已经部署了一个新的jar。为了避免这种情况,当你在更新依赖 JAR 文件时,确保重新编译你的程序。

8. String的概念

生成一个String有两种办法,一种是用引号,另一种是new
用引号创建的字符串,指向的地址是String池中String的地址,而用new创建字符串的话,会在堆中先创建一个对象,而该对象则包含一个指向String池中对应String的地址。
看代码:


public class StringConcept {

    public static void main(String[] args) {
        String hello = "Hello",lo = "lo";
        System.out.println(hello == new String("Hello"));

        System.out.println(hello == "Hello");
        System.out.println(Other.hello == hello);
        System.out.println(hello == "Hel" + "lo");
        System.out.println(hello == "Hel" + lo);
        System.out.println(hello == ("Hel" + lo).intern());
    }


}
class Other{
    public static String hello = "Hello";
}

输出

false //值都是“Hello”,一个用引号创建,一个用new,但是==的值是false
true
true  //这两个true都验证了引号创建的字符串指向String池
true  //由常量表达式计算出的字符串是在编译时进行计算,然后被当作常量;
false //在运行时通过连接计算出的字符串是新创建的,因此是不同的;
true  //通过计算生成的字符串显示调用intern方法后产生的结果与原来存在的同样内容的字符串常量是一样的。

我们可以确定,使用字符串字面量连接的字符串吧,在编译器会被计算,以常量形式表示。
而由引用连接的字符串(”Hel” + lo),是Java运行期间执行的,创建的对象其实是处于堆中的

9. List、Set、Map 和 Queue 之间的区别

List 是一个有序集合,允许元素重复。它的某些实现可以提供基于下标值的常量访问时间,但是这不是 List 接口保证的。Set 是一个无序集合。

10 poll() 方法和 remove() 方法的区别

poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。

11.Java 中 LinkedHashMap 和 PriorityQueue 的区别是什么?

PriorityQueue 保证最高或者最低优先级的的元素总是在队列头部,但是 LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个 PriorityQueue 时,没有任何顺序保证,但是 LinkedHashMap 课保证遍历顺序是元素插入的顺序。

12.ArrayList 与 LinkedList 的区别?

作为列表实现,ArrayList和LinkedList都是有序的,基于索引并允许复制。尽管来自相同类型的层次结构,但这两个类之间有很多不同之处。
ArrayList与LinkedList之间的主要区别是前者是由一个数组支持的,而后者是基于链表数据结构的,这使得add()、remove()、contains()和iterator()的性能不同于ArrayList和LinkedList。

在Java中使用ArrayList vs LinkedList

在比较ArrayList和LinkedList的差异之前,让我们看看在Java中ArrayList和LinkedList之间有什么共同之处:
1)ArrayList和LinkedList都是List接口的实现
2)ArrayList和LinkedList都是不同步的,这意味着在没有外部同步的情况下,不能在多个线程之间共享它们。
3)ArrayList和LinkedList是有序集合,例如它们维护元素的插入顺序,即第一个元素将被添加到第一个位置。
4)ArrayList和LinkedList也允许重复和null,不像其他的列表实现,例如Vector。
5)LinkedList和ArrayList的迭代器都是快速失败(fail-fast),这意味如果在创建迭代器时从结构上修改了集合,会抛出ConcurrentModificationException。它们与CopyOnWriteArrayList不同,CopyOnWriteArrayList 的迭代器是安全失败(fail-safe)的。

在Java中LinkedList和ArrayList的区别

1)基础数据结构:
ArrayList和LinkedList之间的第一个区别是,ArrayList是由数组支持的,而LinkedList是基于链表数据结构。这将导致性能上的进一步差异。
2)LinkedList实现双端队列(Deque)
ArrayList和LinkedList之间的另一个区别是,除了List接口之外,LinkedList还实现了Deque接口
LinkedList被实现为一个双向链表,并且基于索引,因此可以从任意端进行导航
3)ArrayList中添加元素
在ArrayList中添加元素是O(1)操作,如果它不触发调整大小,在这种情况下,它就变成O(log(n)),另一方面,在LinkedList中附加一个元素是O(1)操作,因为它不需要任何导航。
4)从某个位置删除元素
为了从一个特定的索引中移除一个元素,例如通过调用remove(index), ArrayList执行一个复制操作,该操作使它接近O(n),而LinkedList需要遍历到这个点,但是因为它可以从两个方向遍历,所以他的时间复杂度是O(n/2)
5)迭代ArrayList或LinkedList。
迭代是LinkedList和ArrayList的O(n)操作,其中n是元素的数目。
6)从一个位置检索元素
在ArrayList中,get(index)操作是O(1),而在LinkedList中是O(n/2),因为它需要遍历到该条目。
7) Memory
LinkedList使用一个包装器对象:Entry,它是一个静态嵌套类,用于存储数据和两个节点,而ArrayList只是将数据存储在数组中。
因此,在ArrayList的情况下,内存需求似乎比LinkedList更少,除非数组在将内容从一个数组复制到另一个数组时执行重新大小操作。
但是如果数组足够大,那么在那个点可能需要大量内存并触发垃圾收集,这会导致响应时间变慢。

13. ArrayList 和 HashMap 的默认大小是多数?

ArrayList的默认大小是10
HashMap 的默认大小是16个元素(必须是2的幂),最大是1<<30.

14.找出数组中重复的数据

String[] temp = {"1","2","3","2"};
        //方法1
        HashSet<String> set1 = new HashSet<>();
        for(int i = 0;i < temp.length;i++){
            for(int j = i + 1;j < temp.length;j++){
                if(temp[i].equals(temp[j])){
                    set1.add(temp[i]);
                    break;
                }
            }
        }

        //方法2
        HashSet<String> set2 = new HashSet<>();
        HashSet<String> contrast = new HashSet<>();
        for (String number : temp) {
            if(contrast.add(number) == false){
                set2.add(number);
            }
        }

15 Integer的常量池问题

//Integer
        Integer i1 = 10;
        Integer i2 = 10;
        Integer i3 = 0;
        Integer i4 = new Integer(10);
        Integer i5 = new Integer(10);
        Integer i6 = new Integer(0);
        //因为Integer维持了-128~127256个数值,所以如果检测到所设置的数值在该区间,那么直接使用常量池的对象,所以这个两个指向的地址是相同的
        Assert.assertTrue(i1 == i2);
        //Java中的计算是在内存中做拆箱操作后进行比较,其实做的是(40 == 40 + 0)的比较
        Assert.assertTrue(i1 == i2 + i3);
        //同上
        Assert.assertTrue(i4 == i5 + i6);
        //这两个是new创建的,他们指向的是堆地址
        Assert.assertFalse(i4 == i5);

        Assert.assertFalse(i1 == i4);
        Integer i11 = 190;
        Integer i12 = 190;
        //当我们设置的数值大于127时候,我们会发现,这两个值就不再相等了。因为自动装箱操作,他们是对象,有堆地址
        Assert.assertFalse(i11 == i12);

Collection中的同步的集合与并发的集合的区别?

尽管同步和并发的集合类都提供了线程安全,但是它们之间的区别在于性能、可伸缩性以及如何实现线程安全。同步的HashMap、Hashtable、HashSet、Vector和Synchronized ArrayList等同步集合的速度,要比ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteHashSet这样的并发集合慢的多。慢的主要原因是锁;同步集合会锁定整个集合,例如整个映射或列表,而并发集合永远不会锁定整个映射或列表。他们使用先进的和复杂的技术,如锁剥离,来实现线程安全。例如,ConcurrentHashMap将整个映射划分为几个段,只锁定相关的段,这允许多个线程在不锁定的情况下访问相同ConcurrentHashMap的其他段。
类似地,CopyOnWriteArrayList允许多个读取线程在不同步的情况下读取数据,当一个写入发生时,它会复制整个ArrayList并与这个新的副本做交换。

同步集合vs并发集合

同步的集合类Hashtable和Vector,以及同步包装类,Collections.synchronizedMap() 和Collections.synchronizedList(),提供一个基本的、有条件的线程安全实现映射和列表。
然而,有几个因素使得它们不适合在高并发应用程序中使用,它的锁影响了它的可伸缩性,而我们也经常需要在迭代时锁定集合很长时间,以防止ConcurrentModificationException的出现。
ConcurrentHashMap和CopyOnWriteArrayList实现提供了更高的并发性和可伸缩性,通过一些微小的妥协来保护线程安全。
我们知道HashTable是线程安全的,那么它与ConcurrentHashMap的区别是什么呢?HashTable在数据变大时,性能会显著下降。因为ConcurrentHashMap引入了分割的概念,所以数据的大小并不会影响它,因为只有特定的部分被锁定以提供线程安全,所以许多其他的读取操作仍然可以在不等待迭代完成的情况下访问map。

这就是在Java中同步和并发集合类的区别。总之,并发集合使用高级技术实现线程安全,而不影响可伸缩性。例如,ConcurrentHashMap只在进行迭代或执行任何写操作时锁定映射的特定部分,而Hashtable锁定完整的映射。

17. ForkJoin框架

顾名思义,它将一个任务分成几个小任务,作为一个新的fork,当所有子任务完成时,它就会join所有的fork。Fork/join任务是“纯”内存算法,其中没有I/O操作出现。它是基于工作窃取算法的。
为了更快地完成一些事情,java提供了并发概念,但是处理并发并不容易,因为我们必须处理线程同步和共享数据。当我们需要处理一小段代码的时候,处理并发和原子性是很容易的。但是,当代码库和线程数量增加时,它就变得复杂了,它真正具有挑战性的地方是,有几个线程在一起工作以完成一项大任务,所以java尝试使用Executors 和Thread Queue来简化这个并发性。
当我们将Executors 与以前的Thread进行比较时,会发现它使并发任务的管理变得非常简单,它可以处理分治算法,并创建子任务并相互通信以完成任务。但是Executors 框架的问题是Callable 可以自由的向他的Executors 提交一个新的子任务,并以同步或者异步的形式等待子任务的执行结果。问题在于并行性:当一个Callable等待另一个Callable的结果时,它处于等待状态,这样就浪费了处理其他Callable的机会。

为了解决这个问题,java 7给出了并行的概念。在java.util.concurrent中添加了新的fork-join框架。创建了新的fork - join执行器框架负责解决我们上面出现的并行问题。在内部,它维护一个线程池,executor将待完成的任务分配给这个线程池,当一个任务等待另一个任务完成时完成。fork-join框架的整体思想是利用现代计算机的多个处理器。

如何使用fork-join框架编码

fork -join功能是由ForkjoinTask对象实现的,它有两个方法fork()和join()方法。
1)fork()方法允许从现有的ForkJoinTask上分出一个新的ForkJoinTask
2)join()方法允许一个ForkJoinTask等待另一个的完成。

ForkjoinTask对象有两种类型:RecursiveAction 和RecursiveTask ,这是该实例的一种更为专门化的形式。当执行是RecursiveAction 类型的话,那么它并不会yield返回值,而如果执行的是RecursiveTask 类型的,它会yield返回值

18 继承和组合之间有什么不同?

虽然两种都可以实现代码复用,但是组合比继承共灵活,因为组合允许你在运行时选择不同的实现。用组合实现的代码也比继承测试起来更加简单

19.OOP 中的 组合、聚合和关联有什么区别?

在面向对象编程中,一个对象与其他对象相关,以使用该对象提供的功能和服务。这两个对象之间的关系被称为面向对象的通用软件设计中的关联.
而对于组合和聚合,关系比较微妙,他们都是标识一个对象对于另一个对象的关联,但是组合(composition )表示的是一个对象持有另一个对象的引用,而另一个对象是不可以独自存在的。比如人就是一个组合,它由手、脚、胳膊等组成,而这些都不能独自存在。而对于聚合,表示的则是那么一种弱引用,所引用的对象可以独自存在,一个=例子是学生与学校的关系,当学校关闭时,学生仍然存在,然后可以加入另一所学校。
简而言之,两个对象之间的关系被称为关联,当一个对象拥有另一个对象时,一个关联被称为组合,当一个对象使用另一个对象时,关联被称为聚合。
有时为了明确标识组合而非聚合关系,我们可以使用final关键字,来进一步警示。

猜你喜欢

转载自blog.csdn.net/qq_31179577/article/details/79426123