JAVA 面试题 合辑(一)https://blog.csdn.net/haponchang/article/details/92741553
JAVA 面试题 合辑(二)JAVA 面试题 合辑(二)_haponchang的博客-CSDN博客
JAVA 面试题 合辑(三)https://blog.csdn.net/haponchang/article/details/92833016
目录
==与equals()方法的区别?为什么重写equals()方法就必须重写hashCode()方法?
String、StringBuilder、StringBuffer区别
String类能被继承吗?被final修饰为什么不可被继承?
Error 和 Exception 的区别 ?CheckedException和RuntimeException 的区别
System.arraycopy() 和 Arrays.copyOf()两者之间的区别?
java 内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等
Synchronized锁是怎么实现的,放在字节码的什么位置
jvm 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代,说说你知道的几种主要的jvm 参数。
32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?32 位和 64 位的 JVM,int 类型变量的长度是多数?
说说你知道的几种 HTTP 响应码,比如 200, 302, 404。
简述 Http 请求 get 和 post 的区别以及数据包格式。
TCP 三次握手和四次挥手的流程,为什么断开连接要 4 次,如果握手只有两次,会出现什么。
常见的缓存策略有哪些,你们项目中用到了什么缓存系统,如何设计的
BeanFactory 和 ApplicationContext 有什么区别
JAVA 基础
==与equals()方法的区别?为什么重写equals()方法就必须重写hashCode()方法?
1. ==在比较基本数据类型时比较的是值,比较两个对象时比较的是地址值。 equals()方法存在于Object类中,Object类中equals()方法底层依赖的是==操作,在所有没有重写equals()的类中,调用equals()其实和使用==的效果一样,也是比较的地址值。String重写了equals(),底层比较的是两个String对应位置的char字符是否==。
2. Object.hashCode()方法是一个本地native方法,返回的是对象引用中存储的对象的内存地址;
基于散列的集合(HashSet、HashMap和Hashtable)存放key时,调用该对象(存入对象)的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中存储的位置;
所以如果equals方法返回true,那么两个对象的hasCode()返回值必须一样;
String、StringBuilder、StringBuffer区别
不可变:String,底层是 final char value[] 。final类型char数组
subString返回新对象: return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
可变:StringBuffer、StringBuilder,底层是char[] value; 一个char类型的数组,非final类型,这一点与String类不同
构造函数(StringBuffer、StringBuilder相同):
public StringBuffer() {
super(16);//创建一个默认大小为16的char型数组
}
public StringBuffer(int capacity) {
super(capacity);//自定义创建大小为capacity的char型数组
}
线程安全:StringBuffer
线程不安全:StringBuilder
append方法和insert方法的代码,StringBuilder和StringBuffer的方法实现基本上一致,
append,insert,delete方法最根本上都是调用System.arraycopy()这个方法来达到目的
不同的是StringBuffer类的方法前多了个synchronized关键字,即StringBuffer是线程安全的
String类能被继承吗?被final修饰为什么不可被继承?
不能,String被final修饰,不可被继承。原因如下:
1. 字符串常量池的需要:字符串常量池是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象;
2. 允许String对象缓存HashCode:Java中String对象的哈希码被频繁地使用,字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存。这也是一种性能优化手段,意味着不必每次都去计算新的哈希码;
3. 安全性:String常被用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
int和Integer的区别?
Integer是int的包装类。Java 5 中,引入了自动装箱和自动拆箱功能(boxing/unboxing)。
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
在 Java 5 中新增了静态工厂方法 valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进。按照 Javadoc,这个值默认缓存是 -128 到 127 之间。
查看Integer类源码,发现里面有一个私有的静态内部类IntegerCache,而如果直接将一个基本数据类型的值赋给Integer对象,则会发生自动装箱,其原理就是通过调用Integer类的public static Integer valueOf(将int类型的值包装到一个对象中 ,就是在Integer类中有一个静态内部类IntegerCache,在IntegerCache类中有一个Integer数组,用以缓存当数值范围为-128~127时的Integer对象。
注意:可以通过下面这个参数设置最大值:-XX:AutoBoxCacheMax(JDK5)
属性是在使用Oracle/Sun JDK 6,在server模式下,使用:
-XX:AutoBoxCacheMax=NNN
参数即可将Integer的自动缓存区间设置为[-128,NNN]。(-128不可改)
实例化顺序
类加载器实例化时进行的操作步骤:加载–>连接->初始化。
父类静态代变量、父类静态代码块、
子类静态变量、子类静态代码块、
父类非静态变量(父类实例成员变量)、父类构造函数、
子类非静态变量(子类实例成员变量)、子类构造函数。
Object类常用的方法有哪些
1. equals()、hashCode()、getClass()、toString()--默认字符串:类名+哈希编码;
2. clone():实现对象的浅复制(当改变其中一个对象的引用类型属性实例的属性时,另一个对象相应的引用类型的属性实例中的属性也会发生变化),只有实现了Cloneable接口才可以调用该方法。否则抛出CloneNotSupportedException;深复制:引用类型属性也要实现clone()方法并显式调用;
3. finalize():用于JVM对象收集;
4. wait():使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断;
5. notify():唤醒在该对象上等待的某个线程;
6. notifyAll():唤醒在该对象上等待的所有线程;
深拷贝和浅拷贝区别
浅拷贝的问题就是两个对象并非独立的,浅拷贝复制引用。如果你修改了其中一个 Person 对象的 Name 对象,那么这次修改也会影响奥另外一个 Person 对象。
public class Person {
private Name name;
private Address address;
public Person(Person originalPerson) {
this.name = originalPerson.name;
this.address = originalPerson.address;
}
[…]
}
不同于浅拷贝,深拷贝是一个整个独立的对象拷贝。如果我们对整个 Person对象进行深拷贝,我们会对整个对象的结构都进行拷贝。
public class Person {
private Name name;
private Address address;
public Person(Person otherPerson) {
this.name = new Name(otherPerson.name);
this.address = new Address(otherPerson.address);
}
[…]
}
抽象类和接口的区别
语法层次抽象类和接口分别给出了不同的语法定义。
设计层次
抽象层次不同,抽象类是对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
跨域不同
抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在”is-a"关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的,仅仅是实现了接口定义的契约而已,"like-a"的关系。
什么是泛型、为什么要使用以及泛型擦除
语法泛型,即“参数化类型”。
创建集合时就指定集合元素的类型,该集合只能保存其指定类型的元素,避免使用强制类型转换。
Java编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程即类型擦除。泛型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。
类型擦除的主要过程如下:
1)将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
2)移除所有的类型参数。
Error 和 Exception 的区别 ?CheckedException和RuntimeException 的区别
Throwable是所有异常的根,java.lang.Throwable
Error是错误,java.lang.Error;Error由Java虚拟机生成并抛出,包括动态链接失败,虚拟机错误等。程序对其不做处理。
Exception是异常,java.lang.Exception 。Exception一般分为Checked异常和Runtime异常,所有RuntimeException类及其子类的实例被称为Runtime异常,不属于该范畴的异常则被称为CheckedException。
CheckedException,Java认为Checked异常都是可以被处理的异常,所以Java程序必须显示处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误无法编译。方法有两种:
1 当前方法知道如何处理该异常,则用try...catch块来处理该异常。
2 当前方法不知道如何处理,则在定义该方法是声明抛出该异常。
RuntimeException,Runtime如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。
System.arraycopy() 和 Arrays.copyOf()两者之间的区别?
两者的区别在于,Arrays.copyOf()不仅仅只是拷贝数组中的元素,在拷贝元素时,会创建一个新的数组对象。而System.arrayCopy只拷贝已经存在数组元素。
如果我们看过Arrays.copyOf()的源码就会知道,该方法的底层还是调用了System.arrayCopyOf()方法。
一些关于Java优化的资料里也推荐使用System.arraycopy来批量处理数组,其本质就是让处理器利用一条指令处理一个数组中的多条记录,只需指定头指针然后就开始循环即可,执行一次指令,指针就后移一个位置。要操作多少个数据就循环多少次即可。
System.arraycopy为 JVM 内部固有方法,它通过手工编写汇编或其他优化方法来进行 Java 数组拷贝,这种方式比起直接在 Java 上进行 for 循环或 clone 是更加高效的。数组越大体现地越明显。
反射的原理
反射机制:所谓的反射机制就是java语言在运行时拥有一项自观的能力。通过这种能力可以彻底的了解自身的情况为下一步的动作做准备。
Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method;其中class代表的时类对 象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组成部分。
使用反射机制,导入java.lang.relfect 包,遵循三个步骤:
第一步是获得你想操作的类的 java.lang.Class 对象
第二步是调用诸如 getDeclaredMethods 的方法
第三步使用 反射API 来操作这些信息
JAVA 集合
Collection和Collections的区别
java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
Java中的集合类及关系
List和Set继承自Collection接口。
Set无序不允许元素重复,只能插入一个null值。HashSet和TreeSet是主要的实现类。
List有序且允许元素重复,可插入null。ArrayList/LinkedList/Vector是主要的实现类。
Map也属于集合系统,但和Collection接口没关系。Map是key对value的映射集合,其中key列就是一个集合。key不能重复,但是value可以重复。HashMap、TreeMap(TreeMap有序原因:实现了SortedMap接口)和Hashtable是三个主要的实现类。
SortedSet和SortedMap接口对元素按指定规则排序,SortedMap是对key列进行排序。
fail-fast 与 fail-safe 机制有什么区别
fail-fast(快速失败):快速失败机制在遍历一个集合时,如果集合内容被修改,会抛出ConcurrentModificationException异常。
fail-safe(安全失败):安全失败机制对集合的任何修改都会在一个复制的集合上进行,因此不会抛出异常。
ArrayList底层实现原理
Arraylist底层基于数组实现,Arraylist底层默认数组初始化大小为10个object数组,添加元素后大于当前数组的长度,则进行扩容,将数组的长度增加原来数组的一半。
elementData = Arrays.copyOf(elementData, newCapacity);
LinkeList底层实现原理
LinkedList 和 ArrayList 一样,都实现了 List 接口,但其内部的数据结构有本质的不同。LinkedList 是基于链表实现的(通过名字也能区分开来),所以它的插入和删除操作比 ArrayList 更加高效。但也是由于其为基于链表的,所以随机访问的效率要比 ArrayList 差。
LinkedList底层的数据结构是基于双向循环链表的,且头结点中不存放数据。
LinkedList还实现了Queue接口,所以他还提供了offer(),peek(), poll()等方法。
ArrayList和LinkeList区别
内存存储区别
数组从栈中分配空间, 对于程序员方便快速,但自由度小。
链表从堆中分配空间, 自由度大但申请管理比较麻烦.
逻辑结构区别
数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。
链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项)
总结
1、存取方式上,数组可以顺序存取或者随机存取,而链表只能顺序存取;
2、存储位置上,数组逻辑上相邻的元素在物理存储位置上也相邻,而链表不一定;
3、存储空间上,链表由于带有指针域,存储密度不如数组大;
4、按序号查找时,数组可以随机访问,时间复杂度为O(1),而链表不支持随机访问,平均需要O(n);
5、按值查找时,若数组无序,数组和链表时间复杂度均为O(1),但是当数组有序时,可以采用折半查找将时间复杂度降为O(logn);
6、插入和删除时,数组平均需要移动n/2个元素,而链表只需修改指针即可;
7、空间分配方面:
数组在静态存储分配情形下,存储元素数量受限制,动态存储分配情形下,虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且如果内存中没有更大块连续存储空间将导致分配失败;
链表存储的节点空间只在需要的时候申请分配,只要内存中有空间就可以分配,操作比较灵活高效;
ArrayList和Vector区别
ArrayList和Vector都实现了List接口,都是通过数组实现的。
Vector是线程安全的,而ArrayList是非线程安全的。
ArrayList,Vector主要区别为以下几点:
(1):Vector是线程安全的,源码中有很多的synchronized可以看出,而ArrayList不是。导致Vector效率无法和ArrayList相比;
(2):ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,ArrayList默认增加为原来的50%,Vector默认增加为原来的一倍;
(3):Vector可以设置capacityIncrement,而ArrayList不可以,从字面理解就是capacity容量,Increment增加,容量增长的参数。
LinkedList的get(i)方法是怎样实现的?
LinkedList,链表里边的get(i)方法里有一个很有意思的设计模式,二分查找法。有先判断index与size>>1的大小,如果小于size的2分之一,则从前面开始查找,如果大于size的2分之一,则从后面开始查找,减少了一半的时间。
HashMap实现原理
底层:
HashMap底层实现还是数组,只是数组的一个元素可能是一个单链表(哈希冲突时才是链表)。
HashMap的底层结构是由数组+链表构成的。
初始容量默认为16,负载因子默认为0.75
初始容量:当构造一个hashmap时,初始容量设为不小于指定容量的2的次方的一个数(new HashMap(5), 指定容量为5,那么实际初始容量为8,2^3=8>5),且最大值不能超过2的30次方。
扩容:当哈希数组中的条目数超出了加载因子与初始容量的乘积时,则要对该哈希数组进行扩容操作(即resize)。
负载因子越小,容易扩容,浪费空间,但查找效率高 链表特别短 减少hash冲突
负载因子越大,不易扩容,对空间的利用更加充分,查找效率低(链表拉长)hash冲突比较多,链表比较长
HashMap在扩容时,新数组的容量将是原来的2倍,由于容量发生变化,原有的每个元素需要重新计算数组索引Index,再存放到新数组中去,这就是所谓的rehash。
1. HashMap只允许一个为null的key。
2. HashMap的扩容:当前table数组的两倍
3. HashMap实际能存储的元素个数: capacity * loadFactor
4. HashMap在扩容的时候,会重新计算hash值,并对hash的位置进行重新排列, 因此,为了效率,尽量给HashMap指定合适的容量,避免多次扩容
当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。
在Java 8 中,如果一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是 8 ),就使用红黑树来替换链表,从而提高速度。这个替换的方法叫 treeifyBin() 即树形化。
插入:
put过程是先计算hash然后通过hash与table.length取摸计算index值,然后将key放到table[index]位置,当table[index]已存在其它元素时(哈希冲突),会在table[index]位置形成一个链表,将新添加的元素放在table[index],原来的元素通过Entry的next进行链接(新值链头,原值后移)--哈希冲突的解决方案;
获取:
先根据key的hash值得到这个元素在数组中的位置,然后通过key的equals()在链表中找到key对应的Entry节点;
HashMap为什么用红黑树?用其它树不行么?
红黑树牺牲了一些查找性能 但其本身并不是完全平衡的二叉树。因此插入删除操作效率略高于AVL树; AVL树用于自平衡的计算牺牲了插入删除性能,但是因为最多只有一层的高度差,查询效率会高一些。实际应用中,若搜索的次数远远大于插入和删除,那么选择AVL,如果搜索,插入删除次数几乎差不多,应该选择RB。
HashTable实现原理
和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。
HashMap和HashTable区别
- HashTable的方法前面都有synchronized来同步,是线程安全的;HashMap未经同步,是非线程安全的。
- HashTable不允许null值(key和value都不可以) ;HashMap允许nul l值(key和value都可以)。
- HashTable有一个contains(Object value)功能和containsValue(Object value)功能一样。
- HashTable使用Enumeration进行遍历;HashMap使用Iterator进行遍历
- HashTable中hash数组默认大小是11,增加的方式是old*2+1;HashMap中hash数组的默认大小是16,而且一定是2的指数。
- 哈希值的使用不同,HashTable直接使用对象的hashCode; HashMap重新计算hash值,而且用与代替求模。
Iterator
Iterator的API
关于Iterator主要有三个方法:hasNext()、next()、remove()。
hasNext:没有指针下移操作,只是判断是否存在下一个元素
next:指针下移,返回该指针所指向的元素
remove:删除当前指针所指向的元素,一般和next方法一起用,这时候的作用就是删除next方法返回的元素
Java中的Iterator是一种fail-fast的设计。
当Iterator迭代一个容器的时候,如果此时有别的方法在更改Collection(容器)的内容,那么Iterator就会抛出ConcurrentModificationException 。
ConcurrentHashMap实现
要实现线程安全,就需要加锁, ConcurrentMap的做法简单来说, 就是把哈希表分成若干段, 对其中某一段操作时, 只锁住这一段, 其他段可以不受影响。
实现:
a. 整个ConcurrentMap由一个segment数组组成(即segments),数组中每一个segment是一张哈希表, 哈希表中存放的是一张hashentry链表。Segment继承ReentrantLock用来充当锁的角色。
b. 最终存储key,value时是对segment操作, 因此只要对需要插入键值对的segment上锁就可以保证线程安全。
JDK1.8之后的优化:
1. 取消segment字段,直接采用transient volatile HashEntry<K,V> table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率;transient修饰的变量其内容在序列化后无法获得访问;
2. 将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构;
多线程
进程和线程的区别
(1)进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元;
(2)同一个进程中可以包括多个线程,并且线程共享整个进程的资源;
(3)线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源;
多线程的实现方式?start()后线程立即启动么?
继承Thread类、实现Runnable接口、线程池(Cache、Fixed、Single、Schedule)、实现Callable接口通过FutureTask包装器来创建Thread线程;使用ExecutorService、Callable、Future实现有返回结果的多线程。
不是立刻启动:执行start()之后,线程等待CPU调度,当调度成功时,调用run()才启动线程;
什么是阻塞?阻塞的情况?
阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu时间片,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu时间片转到运行(running)状态。
a. 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
b. 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
c. 其他阻塞:运行(running)的线程执行Thread.sleep(long ms),JVM会把该线程置为阻塞状态。当sleep()状态超时,线程重新转入可运行(runnable)状态。)
如何停止一个线程?
java中有以下3种方法可以终止正在运行的线程:
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
- 使用stop方法强行终止,stop()方法停止线程则是非常暴力的,不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
- 使用interrupt方法中断线程。interrupt()方法是在当前线程中打了一个停止标志,并不是真的停止线程。Thread.java类中提供了两种方法:this.interrupted(): 测试当前线程是否已经中断;this.isInterrupted(): 测试线程是否已经中断;
什么是线程安全与非线程安全?如何保证线程安全?
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染;线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据;
线程安全特性:
- 原子性:相关操作不会被其他线程所打扰,一般通过同步机制实现。
- 可见性:一个线程修改了某个共享变量,其状态能够立即被其他线程知晓。通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。
- 有序性:保证线程内的串行语义,避免指令重排等。
保证线程安全:
需要对非安全的代码进行加锁控制;
使用线程安全的类;
多线程并发情况下,线程共享的变量改为方法级的局部变量。
线程生命周期
新建(new Thread):当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。t = new Thread();
就绪(runnable):线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。t.start();
运行(running):线程获得CPU资源正在执行任务(run()方法);
死亡(dead):当线程执行完毕、发生异常或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。

本文汇总了JAVA面试中的基础、集合、多线程、JVM、HTTP及Servlet等多个领域的核心知识点,包括==与equals()的区别、String与StringBuilder、线程安全、线程池、垃圾回收机制、HTTP原理等,旨在帮助Java开发者全面准备面试。
最低0.47元/天 解锁文章
3820

被折叠的 条评论
为什么被折叠?



