【面试】java面试题集锦(一)
11. ConcurrentHashMap的实现原理
ConcurrentHashMap是支持并发读写的HashMap,它的特点是读取数据时无需加锁,写数据时可以保证加锁粒度尽可能的小。由于其内部采用“分段存储”,只需对要进行写操作的数据所在的“段”进行加锁。关于ConcurrentHashMap底层实现的详细分析请参考 Java并发编程:并发容器之ConcurrentHashMap
12. TreeMap, LinkedHashMap, HashMap的区别是什么?
- HashMap
HashMap的底层实现是散列表,因此它内部存储的元素是无序的;
- TreeMap
TreeMap的底层实现是红黑树,所以它内部的元素的有序的。排序的依据是自然序或者是创建TreeMap时所提供的比较器(Comparator)对象。
- LinkedHashMap
LinkedHashMap能够记住插入元素的顺序。
13. Collection与Collections的区别是什么?
- Collection<E>
Collection<E>是Java集合框架中的基本接口;
- Collections
Collections是Java集合框架提供的一个工具类,其中包含了大量用于操作或返回集合的静态方法。
对Java集合框架还不太熟悉的小伙伴请参考 Java核心技术点之集合框架
14、对于“try-catch-finally”,若try语句块中包含“return”语句,finally语句块会执行吗?
答案是会执行。只有两种情况finally块中的语句不会被执行
- 调用了 System.exit() 方法;
- JVM“崩溃”了。
15. Java中的异常层次结构
- Java中的异常层次结构如下图所示:
我们可以看到Throwable类是异常层级中的基类。Error类表示内部错误,这类错误使我们无法控制的;Exception表示异常,RuntimeException及其子类属于未检查异常,这类异常包括ArrayIndexOutOfBoundsException、NullPointerException等,我们应该通过条件判断等方式语句避免未检查异常的发生。IOException及其子类属于已检查异常,编译器会检查我们是否为所有可能抛出的已检查异常提供了异常处理器,若没有则会报错。对于未检查异常,我们无需捕获(当然Java也允许我们捕获,但我们应该做的事避免未检查异常的发生)。
16. Java面向对象的三个特征与含义
三大特征:封装、继承、多态。
- 封装
封装:封装也称信息隐藏,是指利用抽象数据类型把数据和基于数据的操作封装起来,使其成为一个不可分割的整体,数据隐藏在抽象数据内部,尽可能的隐藏数据细节,只保留一些接口使其与外界发生联系。也就是说用户无需知道内部的数据和方法的具体实现细节,只需根据留在外部的接口进行操作就行。
- 封装的好处
- 实现了专业的分工
- 良好的封装能够减少耦合
- 类内部的结构能够自有修改
- 可以对成员进行更精确的控制
- 隐藏信息,实现细节
- 继承
- Java继承是面向对象的最显著的一个特征。继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。JAVA不支持多继承,单继承使JAVA的继承关系很简单,一个类只能有一个父类,易于管理程序,父类是子类的一般化,子类是父类的特化(具体化)
- 继承避免了对一般类和特殊类之间共同特征进行的重复描述。同时,通过继承可以清晰地表达每一项共同特征所适应的概念范围——在一般类中定义的属性和操作适应于这个类本身以及它以下的每一层特殊类的全部对象。运用继承原则使得系统模型比较简练也比较清晰。
- 继承关系是传递的。若类C继承类B,类B继承类A(多继承),则类C既有从类B那里继承下来的属性与方法,也有从类A那里继承下来的属性与方法,还可以有自己新定义的属性和方法。继承来的属性和方法尽管是隐式的,但仍是类C的属性和方法。
- 继承提供了软件复用功能。若类B继承类A,那么建立类B时只需要再描述与基类(类A)不同的少量特征(数据成员和成员方法)即可。这种做法能减小代码和数据的冗余度,大大增加程序的重用性。
- 继承通过增强一致性来减少模块间的接口和界面,大大增加了程序的易维护性。
- 多态
方法的重写、重载与动态连接构成多态性
Java之所以引入多态的概念,原因之一是它在类的继承问题上和C++不同,后者允许多继承,这确实给其带来的非常强大的功能,但是复杂的继承关系也给C++开发者带来了更大的麻烦,为了规避风险,Java只允许单继承,派生类与基类间有IS-A的关系(即“猫”is a “动物”)。这样做虽然保证了继承关系的简单明了,但是势必在功能上有很大的限制,所以,Java引入了多态性的概念以弥补这点的不足,此外,抽象类和接口也是解决单继承规定限制的重要手段。同时,多态也是面向对象编程的精髓所在。
- 向上转型
定义一个父类类型的引用指向一个子类的对象既可以使用子类强大的功能,又可以抽取父类的共性。
- 父类引用只能调用父类中存在的方法和属性,不能调用子类的扩展部分;因为父类引用指向的是堆中子类对象继承的父类;(但是如果强制把超类转换成子类的话,就可以调用子类中新添加而超类没有的方法了。)
- 重写
- 父类中的一个方法只有在父类中定义而在子类中没有重写的情况下,才可以被父类类型的引用调用;
- 对于父类中定义的方法,如果子类中重写了该方法,那么父类类型的引用将会调用子类中的这个方法,这就是动态连接。
17. Override, Overload的含义与区别
-
**Override(重写,覆盖) **
- 覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;
- 覆盖的方法的返回值必须和被覆盖的方法的返回一致;
- 覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;
- 被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
- 存在于父类和子类之间。
- 方法被定义为final不能被重写。
-
**Overload(重载,过载) **
- 在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是fun(int, float), 但是不能为fun(int, int));
- 不能通过访问权限、返回类型、抛出的异常进行重载;
- 方法的异常类型和数目不会对重载造成影响;
- 同类中。
18. 接口与抽象类的区别
接口是一种约定,实现接口的类要遵循这个约定;抽象类本质上是一个类,使用抽象类的代价要比接口大。
- 接口与抽象类的对比如下:
-
抽象类中可以包含属性,方法(包含抽象方法与有着具体实现的方法),常量;接口只能包含常量和方法声明。
-
抽象类中的方法和成员变量可以定义可见性(比如public、private等);而接口中的方法只能为public(缺省为public)。
-
一个子类只能有一个父类(具体类或抽象类);而一个接口可以继承一个多个接口,一个类也可以实现多个接口。
-
子类中实现父类中的抽象方法时,可见性可以大于等于父类中的;而接口实现类中的接口 方法的可见性只能与接口中相同(public)。
-
19. 静态内部类与非静态内部类的区别
-
内部静态类不需要有指向外部类的引用。但非静态内部类需要持有对外部类的引用。
-
非静态内部类能够访问外部类的静态和非静态成员。静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员。
-
一个非静态内部类不能脱离外部类实体被创建,一个非静态内部类可以访问外部类的数据和方法,因为他就在外部类里面。
-
** 示例**
/* 下面程序演示如何在java中创建静态内部类和非静态内部类 */
class OuterClass{
private static String msg = "GeeksForGeeks";
// 静态内部类
public static class NestedStaticClass{
// 静态内部类只能访问外部类的静态成员
public void printMessage() {
// 试着将msg改成非静态的,这将导致编译错误
System.out.println("Message from nested static class: " + msg);
}
}
// 非静态内部类
public class InnerClass{
// 不管是静态方法还是非静态方法都可以在非静态内部类中访问
public void display(){
System.out.println("Message from non-static nested class: "+ msg);
}
}
}
class Main
{
// 怎么创建静态内部类和非静态内部类的实例
public static void main(String args[]){
// 创建静态内部类的实例
OuterClass.NestedStaticClass printer = new OuterClass.NestedStaticClass();
// 创建静态内部类的非静态方法
printer.printMessage();
// 为了创建非静态内部类,我们需要外部类的实例
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
// 调用非静态内部类的非静态方法
inner.display();
// 我们也可以结合以上步骤,一步创建的内部类实例
OuterClass.InnerClass innerObject = new OuterClass().new InnerClass();
// 同样我们现在可以调用内部类方法
innerObject.display();
}
}
20. Java中多态的实现原理
所谓多态,指的就是父类引用指向子类对象,调用方法时会调用子类的实现而不是父类的实现。多态的实现的关键在于“动态绑定”。
21. 简述Java中创建新线程的两种方法
-
继承Thread类(假设子类为MyThread),并重写run()方法,然后new一个MyThread对象并对其调用start()即可启动新线程。
-
实现Runnable接口(假设实现类为MyRunnable),而后将MyRunnable对象作为参数传入Thread构造器,在得到的Thread对象上调用start()方法即可。
22. 简述Java中进行线程同步的方法
-
volatile: Java Memory Model保证了对同一个volatile变量的写happens before对它的读;
-
synchronized: 可以来对一个代码块或是对一个方法上锁,被“锁住”的地方称为临界区,进入临界区的线程会获取对象的monitor,这样其他尝试进入临界区的线程会因无法获取monitor而被阻塞。由于等待另一个线程释放monitor而被阻塞的线程无法被中断。
-
ReentrantLock: 尝试获取锁的线程可以被中断并可以设置超时参数。
23. 简述Java中具有哪几种粒度的锁
Java中可以对类、对象、方法或是代码块上锁。更加详细的介绍请戳 Java核心技术点之多线程
24. 给出“生产者-消费者”问题的一种解决方案
使用阻塞队列:
public class BlockingQueueTest {
private int size = 20;
private ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(size);
public static void main(String[] args) {
BlockingQueueTest test = new BlockingQueueTest();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();
producer.start();
consumer.start();
}
class Consumer extends Thread {
@Override
public void run() {
while (true) {
try { // 从阻塞队列中取出一个元素
queue.take();
System.out.println("队列剩余" + queue.size() + "个元素");
} catch (InterruptedException e) {
}
}
}
}
class Producer extends Thread {
@Override
public void run() {
while (true) {
try { // 向阻塞队列中插入一个元素
queue.put(1);
System.out.println("队列剩余空间:" + (size - queue.size()));
} catch (InterruptedException e) {
}
}
}
}
}
25. ThreadLocal的设计理念与作用
请关注,共同进步ThreadLocal的作用是提供线程内的局部变量,在多线程环境下访问时能保证各个线程内的ThreadLocal变量各自独立。也就是说,每个线程的ThreadLocal变量是自己专用的,其他线程是访问不到的。ThreadLocal最常用于以下这个场景:多线程环境下存在对非线程安全对象的并发访问,而且该对象不需要在线程间共享,但是我们不想加锁,这时候可以使用ThreadLocal来使得每个线程都持有一个该对象的副本。
关于ThreadLocal的实现原理分析请戳 Java核心技术点之动态代理