JavaSE 基础面试题

JDK 和 JRE 的区别是什么?

Java 运行时环境 (JRE)。它包括 Java 虚拟机、Java 核心类库和支持文件。它不包含开发工具(JDK)–编译器、调试器和其他工具。

Java 开发工具包 (JDK) 是完整的 Java 软件开发包,包含了 JRE,编译器和其他的工具(比如:JavaDoc,Java 调试器),可以让开发者开发、编译、执行 Java 应用程序。

float f = 3.4;是否正确?

不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换 float f =(float)3.4; 或者写成 float f =3.4F;。

int 和 Integer 有什么区别?

Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。

Java 为每个原始类型提供了包装类型:
- 原始类型: boolean,char,byte,short,int,long,float,double
- 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
有关的一个面试题 (这个题目挺有意思的,大家要多加注意):

public class Test03 {

    public static void main(String[] args) {
        Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;

        System.out.println(f1 == f2);
        System.out.println(f3 == f4);
    }
}

如果不明就里很容易认为两个输出要么都是 true 要么都是 false。首先需要注意的是 f1、f2、f3、f4 四个变量都是 Integer 对象引用,所以下面的==运算比较的不是值而是引用。装箱的本质是什么呢?当我们给一个 Integer 对象赋一个 int 值的时候,会调用 Integer 类的静态方法 valueOf,如果看看 valueOf 的源代码就知道发生了什么。

public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

IntegerCache 是 Integer 的内部类,其代码如下所示:

/**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

简单的说,如果整型字面量的值在-128 到 127 之间,那么不会 new 新的 Integer 对象,而是直接引用常量池中的 Integer 对象,所以上面的面试题中 f1==f2 的结果是 true,而 f3==f4 的结果是 false。

== 与 equals() 的区别?

== 比较的是两个变量之间的值是否相等,类比讲的话也就是说比较这两个对象是不是同一个人。基本数据类型比较的是两个变量的值;如果是引用数据类型,比较的是对应引用类型的首地址。

public class TestString {
    public static void main(String[] args) {

        Integer a1 = 1;
        Integer a2 = 1;
        Integer b1 = 150;
        Integer b2 = 150;

        System.out.println(a1 == a2);
        System.out.println(b1 == b2);
    }
}

这个例子你可以类比上面的那个题目,最好两个题目都看一下,a1 == a2 的结果为 true ,因为他们两个指向的是同一个对象。b1 == b2 结果为 false,因为 b1 与 b2 是两个不同的对象所以它们的首地址并不相同故结果为 false。至于 a1 与 a2 也是引用类型的数据为什么比较的结果是 true,这是由 Integer 包装类的特性决定的,你可以参考上面的题目。

equals() 比较的是两个对象对否具有某些一样的特征,类比讲的话也就是比较这两个人是否一样。其实 Object 类中的 equals() 方法调用的就是 this == obj,你可以通过重写 equals() 方法自定义你想要比较的某些特征,但是要注意的是如果你重写了 equals() 方法,那么也要重写 hashCode() 方法。

switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?

在 Java 5 以前,switch(expr) 中,expr 只能是 byte、short、char、int(及其包装类型)。从 Java 5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。 当你指定 expr 为 long 时会在编译期获得一个错误:

required: ‘char, byte, short, int, Character, Byte, Short, Integer, String, or an enum

在一个技术群里看到一个朋友说他在面试的时候遇到一个有意思的题目,问 if() 中写什么条件会打印 AB?

public class PrintAB {
    public static void main(String[] args) {

        if( ? ){
            System.out.print("A");
        }else {
            System.out.print("B");
        }
    }
}

System.out.printf(“A”) == null ,我们来看一看 System.out.printf() 在源码中的实现,这个方法返回了一个 PrintStream 打印流对象,并且这个方法允许传入可变的参数列表。在执行的时候需要注意打印的是 if() 条件里面的‘’A‘’(不得不吐槽一句,现在都这么会玩的吗)。

public PrintStream printf(String format, Object ... args) {
        return format(format, args);
    }

如果两个对象经过判断 (x.equals(y) == true),x 与 y 必须要有相同的 hashCode 吗?

是,官方 API 中 Object 中的 hashCode() 方法中这样说道:

如果根据 equals(Object) 方法判定两个对象是相等的,那么调用 hashCode 方法每一个对象必须产生相同的整数结果。如果按照 equals(java.lang.Object) 方法判定两个对象是不平等的,然后调用 hashCode 方法每一个对象必须产生不同的整数结果。

但是反过来是正确的:如果两个对象 x 与 y 的 hashCode() 相同,那么 x.equals(y) 不一定为 true。如果你了解 HashMap 中的键值存储那么这个问题时很容易理解的。还有一点要注意的是如果你重写了 equals() 方法,那么也要重写 hashCode() 方法。

抽象类与接口

从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

如何权衡是使用无序的数组还是有序的数组?

有序数组最大的好处在于查找的时间复杂度是 O(log n),而无序数组是 O(n)。有序数组的缺点是插入操作的时间复杂度是 O(n),因为值大的元素需要往后移动来给新元素腾位置。相反,无序数组的插入时间复杂度是常量 O(1)。

Iterator 和 ListIterator 的区别是什么?

  • Iterator 可用来遍历 Set 和 List 集合,但是 ListIterator 只能用来遍历 List
  • Iterator 对集合只能是前向遍历,ListIterator 既可以前向也可以后向
  • ListIterator 实现了 Iterator 接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等

HashSet 和 TreeSet 有什么区别?

HashSet 是由一个 hash 表来实现的,因此,它的元素是无序的。add(),remove(),contains() 方法的时间复杂度是 O(1)。

另一方面,TreeSet 是由一个树形的结构来实现的,它里面的元素是有序的。因此,add(),remove(),contains() 方法的时间复杂度是 O(logn)。

HashMap 和 Hashtable 有什么区别?

  • HashMap 允许键和值是 null,而 Hashtable 不允许键或者值是 null
  • Hashtable 是同步的,而 HashMap 不是。因此,HashMap 更适合于单线程环境,而 Hashtable 适合于多线程环境
  • HashMap 提供了可供应用迭代的键的集合,因此,HashMap 是快速失败的。另一方面,Hashtable 提供了对键的列举 (Enumeration)
  • 一般认为 Hashtable 是一个遗留的类

快速失败 (fail-fast) 和安全失败 (fail-safe) 的区别是什么?

Iterator 的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util 包下面的所有的集合类都是快速失败的,而 java.util.concurrent 包下面的所有的类都是安全失败的。快速失败的迭代器会抛出 ConcurrentModificationException 异常,而安全失败的迭代器永远不会抛出这样的异常。

Error 和 Exception 有什么区别?

Error 类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。

Exception 类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

写出 5 个你遇见过的运行时异常

ClassCastException:类转换异常

IndexOutOfBoundsException:数组越界

NullPointerException:空指针

ArrayStoreException:数据存储异常,操作数组时类型不一致

BufferOverflowException :I/O 异常

ConcurrentModificationException:并发修改时抛出

IllegalArgumentException:参数不合适时抛出

IllegalStateException:对象状态不合适时抛出

Java 中的两种异常类型是什么?他们有什么区别?

Java 中有两种异常:受检查的 (checked) 异常和不受检查的 (unchecked) 异常。也可以称为运行时异常RuntimeException 和非运行时异常。

不受检查的异常不需要在方法或者是构造函数上声明,就算方法或者是构造函数的执行可能会抛出这样的异常,并且不受检查的异常可以传播到方法或者是构造函数的外面。

相反,受检查的异常必须要用throws语句在方法或者是构造函数上声明。

异常处理完成以后,Exception 对象会发生什么变化?

Exception对象会在下一个垃圾回收过程中被回收掉。

try{} 里有一个 return 语句,那么紧跟在这个 try 后的 finally{} 里的代码会不会被执行? 有哪些情况下 finally 语句块是不执行的

会执行,如果在 try 里有返回值,并且 finally{} 中也有返回值,那么得到最终的值是 finally 中返回的数据,但是并不建议这么做。下面补充两个 finally{} 中不执行的情况:
1:在 try{} 里执行 system.exit() 语句,表示终止当前虚拟机。
2:守护线程中的 finally{} 不执行 (守护线程执行的情况下)。

public class DaemonSample implements Runnable{
    @Override
    public void run() {
        try{
            System.out.println("daemon started");
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread() + "  " + this);
        }

    }

    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonSample());
        thread.setDaemon(true);
        thread.start();
    }
}

并发与并行的区别

并发:并发是指两个或更多任务可以在重叠时间段内启动,运行并完成。 这并不一定意味着他们都会在同一时刻运行。 例如,在单核机器上进行多任务处理。

并行:并行是指任务在同一时间运行,例如在多核处理器上运行。

Thread 类的 sleep() 方法和对象的 wait() 方法都可以让线程暂停执行,它们有什么区别?

sleep() 方法(休眠)是线程类(Thread)的静态方法, 调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复。wait() 是 Object 类的方法,调用对象的 wait() 方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的 notify() 方法(或 notifyAll() 方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

补充:可能不少人对什么是进程,什么是线程还比较模糊,对于为什么需要多线程编程也不是特别理解。简单的说:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是 CPU 调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它可能占用了更多的 CPU 资源。当然,也不是线程越多,程序的性能就越好,因为线程之间的调度和切换也会浪费 CPU 时间。时下很时髦的 Node.js 就采用了单线程异步 I/O 的工作模式。

什么是线程池(thread pool)

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类 Executors 面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

概括的解释下线程的几种可用状态。

JavaSE 基础面试题_第1张图片
1、新建 ( new ):新创建了一个线程对象。

2、可运行 ( runnable ):线程对象创建后,其他线程 (比如 main 线程)调用了该对象 的 start () 方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取 cpu 的使用权 。

3、运行 ( running ):可运行状态 ( runnable ) 的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。

4、阻塞 ( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行 ( runnable ) 状态,才有 机会再次获得 cpu timeslice 转到运行 ( running ) 状态。阻塞的情况分三种:
(一). 等待阻塞:运行 ( running ) 的线程执行 o . wait () 方法, JVM 会把该线程放 入等待队列 ( waitting queue ) 中。
(二). 同步阻塞:运行 ( running ) 的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则 JVM 会把该线程放入锁池 ( lock pool ) 中。
(三). 其他阻塞: 运行 ( running ) 的线程执行 Thread . sleep ( long ms ) 或 t . join () 方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。 当 sleep () 状态超时、 join () 等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行 ( runnable ) 状态。

5、死亡 ( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run () 方法,则该线程结束生命周期。死亡的线程不可再次复生。

同步方法和同步代码块的区别是什么?

1.同步方法默认用 this 或者当前类 class 对象作为锁

2.同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法

3.同步方法使用关键字 synchronized修饰方法,而同步代码块主要是修饰需要进行同步的代码,用 synchronized(object){代码内容}进行修饰

在监视器 (Monitor) 内部,是如何做线程同步的?程序应该做哪种级别的同步?

在 java 虚拟机中, 每个对象 ( Object 和 class ) 通过某种逻辑关联监视器,每个监视器和一个对象引用相关联, 为了实现监视器的互斥功能, 每个对象都关联着一把锁.

一旦方法或者代码块被 synchronized 修饰, 那么这个部分就放入了监视器的监视区域, 确保一次只能有一个线程执行该部分的代码, 线程在获取锁之前不允许执行该部分的代码

你可能感兴趣的:(JavaSE 基础面试题)