java面试必知必会
- 面向对象
- 成员变量成员方法
- Integer相关
- double 和 Double相关
- 多态,向上转型
- hashcode、==、equals比较
- java中子类继承父类时是否继承构造函数
- public、protect、private、static、final、abstract
- 关键字 static、final、this、super
- 类的加载过程验证
- 一个实例变量在对象初始化的过程中会被赋值几次?
- 实例化的过程
- 对比 Vector、ArrayList、LinkedList 有何区别
- Hashtable、HashMap、TreeMap 有什么不同
- Hashtable、HashMap、TreeMap心得
- 如何保证容器是线程安全的?ConcurrentHashMap 如何实现高效地线程安全?
- Java 提供了哪些 IO 方式? NIO 如何实现多路复用?
- Java 有几种文件拷贝方式?哪一种最高效?
- 谈谈接口和抽象类有什么区别?
- 面向对象设计
- 面向对象编程,掌握基本的设计原则S.O.L.I.D 原则
- 谈谈你知道的设计模式?请手动实现单例模式,Spring 等框架中使用了哪些模式?
- synchronized 底层如何实现?什么是锁的升级、降级?
- 免责声明
面向对象
什么是面向对象?
- 两种主流开发方法,结构化开发和面向对象开发
- 基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计
- 面向对象的3大特征,封装,继承,多态
- 更好的可重用性,可扩展性和可维护性
- 面向对象基本单位是类 成员变量 + 方法 = 类
成员变量成员方法
Integer相关
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == (a+b));
System.out.println(c.equals(a+b));
System.out.println(g == (a+b));
System.out.println(g.equals(a+b));
double 和 Double相关
double i = 0.0/0.0;System.out.println(i==i);
double i2 = 1.0/0.0;System.out.println(i2==i2);
Double di = 0.0/0.0; System.out.println(di == di);
Double di2 = 1.0/0.0; System.out.println(di2 == di2);
多态,向上转型
- 相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态,编译时类型和运行时类型不一致,编译看左边运行看右边
- 把子类对象赋值给父类引用时,称为向上转型。这种转型只是表明这个引用变量的编译是类型是父类,但是实际执行他的方法时,依然表现出子类对象的行为方式。但是把一个父类对象赋值给子类引用变量时,就需要强制类型转换,而且还可能在运行时产生ClassCastException异常。
hashcode、==、equals比较
java中equals,hashcode和==的区别
java中子类继承父类时是否继承构造函数
- 构造函数不能继承,只是调用而已。如果父类没有无参构造函数创建子类时,不能编译,除非在构造函数代码体中第一行,必须是第一行显式调用父类有参构造函数,如果不显示调用父类有参构造函数,系统会默认调用父类无参构造函数super();但是父类中没有无参构造函数,那它不是不能调用了。所以编译就无法通过了
public、protect、private、static、final、abstract
关键字 static、final、this、super
static:关键字,是一个修饰符,用于修饰成员(成员变量和成员函数)。
- 特点:
1、想要实现对象中的共性数据的对象共享。可以将这个数据进行静态修饰
2、被静态修饰的成员,可以直接被类名所调用。也就是说,静态的成员多了一种调用方式。类名.静态方式。
3、静态随着类的加载而加载。而且优先于对象存在。
4、static修饰内部类,普通类是不允许声明为静态的,只有内部类才可以
- 弊端:
1、有些数据是对象特有的数据,是不可以被静态修饰的。因为那样的话,特有数据会变成对象的共享数据。这样对事物的描述就出了问题。所以,在定义静态时,必须要明确,这个数据是否是被对象所共享的。
2、静态方法只能访问静态成员,不可以访问非静态成员。(即static修饰符不能访问没有是static修饰的成员)因为静态方法加载时,优先于对象存在,所以没有办法访问对象中的成员。
3、静态方法中不能使用this,super关键字.因为this代表对象,而静态在时,有可能没有对象,所以this无法使用
this:关键字
- java中提供了this关键字,this关键字总是指向调用该方法的对象
- this可以代表任何对象,当this出现在某个方法体中的时候,他所代表的对象是不确定的,但是他的类型是确定的,他所代表的对象只能是当前类;只有当这个方法被调用时,他所代表的对象才能被确定下来,谁在调用这个方法,this就代表谁。
1、调用本类方法
public String introYourself() {
return this.whoAreU() + this.haoOldAreU();
}
2、调用本类属性
public void changeMyName(String name) {
this.name = name;
}
3、调用本类的其他构造方法
public UserExample(String name) {
this(name, -1);
}
4、调用父类的或指定的其他的类的同名方法,为避免歧义而生的方法
public String whoAreSuper() {
return "super is " + UserExample.this.whoAreU() + ". ";
}
5、隐藏式的调用,为了写代码方便(更常用),不指定范围,java会在全类范围内向上查找变量或方法
public String whoAmI() {
return whoAreU();
}
类的加载过程验证
public class StaticTest2 {
public static void main(String[] args){
new B();
}
static class A{
int a = 3;
static {
System.out.println("父类A静态代码块");
}
{
System.out.println("父类A实例代码块");
}
public A(){
System.out.println("父类A的构造器 "+a);
a = 2;
display();
}
public A(int i){
System.out.println("父类A的带参构造函数"+i);
}
public void display(){
System.out.println("父类A的实例函数: "+a);
}
}
static class B extends A{
static int a = 1;
static {
System.out.println("子类B静态代码块");
}
{
System.out.println("子类B实例代码块");
}
public B(){
super();
System.out.println("子类B的默认构造函数: "+a);
a = 5;
display();
}
public B(int j){
System.out.println("子类B的带参构造函数"+j);
}
@Override
public void display(){
System.out.println("子类B的实例函数 "+a);
}
}
}
输出结果:
一个实例变量在对象初始化的过程中会被赋值几次?
- 我们知道,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次
实例化的过程
参考文章:JVM类生命周期概述:加载时机与加载过程
public class StaticTest {
public static void main(String[] args) {
staticFunction();
}
static StaticTest st = new StaticTest();
static {
System.out.println("1");
}
{
System.out.println("2");
}
StaticTest() {
System.out.println("3");
System.out.println("a=" + a + ",b=" + b);
}
public static void staticFunction() {
System.out.println("4");
}
int a = 110;
static int b = 112;
}
对比 Vector、ArrayList、LinkedList 有何区别
- 这三者都是实现集合框架中的 List,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置进行定位、添加或者删除的操作,都提供迭代器以遍历其内容等。但因为具体的设计区别,在行为、性能、线程安全等方面,表现又有很大不同
- Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据
- ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%
- LinkedList 顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的
Hashtable、HashMap、TreeMap 有什么不同
- Hashtable、HashMap、TreeMap 都是最常见的一些 Map 实现,是以键值对的形式存储和操作数据的容器类型
- Hashtable 是早期 Java 类库提供的一个哈希表实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用
- HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get操作 可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选,比如,实现一个用户ID 和用户信息对应的运行时存储结构
- TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的Comparator 来决定,或者根据键的自然顺序来判断
- 大部分使用 Map 的场景,通常就是放入、访问或者删除,而对顺序没有特别要求,HashMap在这种情况下基本是最好的选择。HashMap 的性能表现非常依赖于哈希码的有效性,请务必掌握 hashCode 和 equals 的一些基本约定:
- LinkedHashMap 通常提供的是遍历顺序符合插入顺序它的实现是通过为条目(键值对)维护一个双向链表
- 对于 TreeMap,它的整体顺序是由键的顺序关系决定的,通过 Comparator 或Comparable(自然顺序)来决定
Hashtable、HashMap、TreeMap心得
三者均实现了Map接口,存储的内容是基于key-value的键值对映射,一个映射不能有重复的键,一个键最多只能映射一个值
- 元素特性
- HashTable中的key、value都不能为null;HashMap中的key、value可以为null,很显然只能有一个key为null的键值对,但是允许有多个值为null的键值对;TreeMap中当未实现 Comparator 接口时,key 不可以为null;当实现 Comparator 接口时,若未对null情况进行判断,则key不可以为null,反之亦然
- 顺序特性
- HashTable、HashMap具有无序特性。TreeMap是利用红黑树来实现的(树中的每个节点的值,都会大于或等于它的左子树种的所有节点的值,并且小于或等于它的右子树中的所有节点的值),实现了SortMap接口,能够对保存的记录根据键进行排序。所以一般需要排序的情况下是选择TreeMap来进行,默认为升序排序方式(深度优先搜索),可自定义实现Comparator接口实现排序方式
- 初始化与增长方式
- 初始化时:HashTable在不指定容量的情况下的默认容量为11,且不要求底层数组的容量一定要为2的整数次幂;HashMap默认容量为16,且要求容量一定为2的整数次幂。
扩容时:Hashtable将容量变为原来的2倍加1;HashMap扩容将容量变为原来的2倍
- 线程安全性
- HashTable其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。也正因为如此,在多线程运行环境下效率表现非常低下。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会进入阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被
阻塞,大大降低了程序的运行效率,在新版本中已被废弃,不推荐使用。
HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步
(1)可以用 Collections的synchronizedMap方法;
(2)使用ConcurrentHashMap类,相较于HashTable锁住的是对象整体,ConcurrentHashMap基于lock实现锁分段技术。首先将Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升
- 一段话HashMap
- HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法用来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构
如何保证容器是线程安全的?ConcurrentHashMap 如何实现高效地线程安全?
- Java 提供了不同层面的线程安全支持。在传统集合框架内部,除了 Hashtable 等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),我们可以调用 Collections 工具类提供的包装方法,来获取一个同步的包装容器(如 Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。
另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了:
- 各种并发容器,比如 ConcurrentHashMap、CopyOnWriteArrayList。
- 各种线程安全队列(Queue/Deque),如 ArrayBlockingQueue、SynchronousQueue。
- 各种有序容器的线程安全版本等
具体保证线程安全的方式,包括有从简单的 synchronize 方式,到基于更加精细化的,比如基于分离锁实现的 ConcurrentHashMap 等并发实现等
Java 提供了哪些 IO 方式? NIO 如何实现多路复用?
Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分。
- 首先,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序
- java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。
- 很多时候,人们也把 java.net 下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为
- 第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式
- 第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作
- IO 都是同步阻塞模式,所以需要多线程以实现多任务处理。而NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel
Java 有几种文件拷贝方式?哪一种最高效?
*Java 有多种比较典型的文件拷贝实现方式,例如利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建一个 FileOutputStream,完成写入工作
public static void copyFileByStream(File source, File dest) throws
IOException {
try (InputStream is = new FileInputStream(source);
OutputStream os = new FileOutputStream(dest);){
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
}
}
或者,利用 java.nio 类库提供的 transferTo 或 transferFrom 方法实现
public static void copyFileByChannel(File source, File dest) throws
IOException {
try (FileChannel sourceChannel = new FileInputStream(source)
.getChannel();
FileChannel targetChannel = new FileOutputStream(dest).getChannel
();){
for (long count = sourceChannel.size() ;count>0 ;) {
long transferred = sourceChannel.transferTo(
sourceChannel.position(), count, targetChannel);
sourceChannel.position(sourceChannel.position() + transferred);
count -= transferred;
}
}
}
对于 Copy 的效率,这个其实与操作系统和配置等情况相关,,总体上来说,NIO transferTo/From 的方式可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换
谈谈接口和抽象类有什么区别?
- 接口和抽象类是 Java 面向对象设计的两个基础机制
- 接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何属性都是隐含着 public static final 的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。Java 标准类库中,定义了非常多的接口,比如 java.util.List
- 抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。Java 标准库中,比如 collection 框架,很多通用部分就被抽取成为抽象类,例如 java.util.AbstractList。
- Java 类实现 interface 使用 implements 关键词,继承 abstract class 则是使用 extends 关键词,我们可以参考 Java 标准库中的 ArrayList
面向对象设计
封装、继承、多态
- 封装的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。我们在日常开发中,因为无意间暴露了细节导致的难缠 bug太多了,比如在多线程环境暴露内部状态,导致的并发修改问题。从另外一个角度看,封装这种隐藏,也提供了简化的界面,避免太多无意义的细节浪费调用者的精力。
- 继承是代码复用的基础机制,类似于我们对于马、白马、黑马的归纳总结。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果
- 你可能立即会想到重写(override)和重载(overload)、向上转型。简单说,重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数,本质上这些方法签名是不一样的
面向对象编程,掌握基本的设计原则S.O.L.I.D 原则
- 单一职责(Single Responsibility),类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。
- 开关原则(Open-Close, Open for extension, close for modification),设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同类功能而修改已有实现,这样可以少产出些回归(regression)问题
- 里氏替换(Liskov Substitution),这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换
- 接口分离(Interface Segregation),我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。
对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果某个接口设计有变,不会对使用其他接口的子类构成影响
- 依赖反转(Dependency Inversion),实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝。
谈谈你知道的设计模式?请手动实现单例模式,Spring 等框架中使用了哪些模式?
大致按照模式的应用目标分类,设计模式可以分为创建型模式、结构型模式和行为型模式
- 创建型模式,是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)
- 结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见的结构型模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等
- 行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)
synchronized 底层如何实现?什么是锁的升级、降级?
- synchronized 代码块是由一对儿monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元
- 在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
- 现代的(Oracle)JDK 中,JVM提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
- 所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
- 当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
- 如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁
免责声明
本文章是作者查阅java面试资料而进行总结汇总的JAVA面试必知必会,如有侵权请联系作者删除