大部分问题来源掘金这篇文章
结合其他博客与相关内容,将问题的参考答案总结在这里,便于读者理解。
[TOC]
java基本类型
- byte、1 字节,最小值-128(-2^7),最大值127(2^7-1)
- short、2 字节,最小值-32768(-2^15),最大值32767(2^15 - 1)
- int、4 字节,最小值是 -2,147,483,648(-2^31),最大值是 2,147, 483,647(2^31 - 1)
- long、8字节,最小值是 -9,223,372,036,854,775,808(-2^63), 最大值是 9,223,372,036,854,775,807(2^63 -1)
- double、8字节,双精度浮点数字长64位,尾数长度52,指数长度11,指数* 偏移量1023
- float、4字节,单精度浮点数字长32位,尾数长度23,指数长度8,指数偏移* 量127
- boolean、至少 1 字节,这种类型只作为一种标志来记录true\false情况
- char、2 字节,最小值\u0000(即为0),最大值\uffff(即为65,535)
值传递和引用传递的区别
-
按值调用(call by value):表示方法接收的是调用着提供的值
-
按引用调用(call by reference):方法接收的是调用者提供的变量地址(如果是C语言的话来说就是指针,当然java并没有指针的概念)
根本区别:方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值
java程序设计语言确实是采用了按值调用,即call by value。也就是说方法得到的是所有参数值的一个拷贝,方法并不能修改传递给它的任何参数变量的内容。但是java区分基本数据类型和引用数据类型(对象引用),而如果传递的是引用数据类型(对象),引用类型对应的值是可以被修改的。比如调用 user 对象的 set方法,这里涉及到内存概念。方法拷贝对象的引用进行传递,但是这两个引用指向的是同一块地址,所以拷贝的引用是可以修改原引用对象所对应的值
更多详细内容请参考博客
== 和 equals 区别是什么
== 比较运算符,如果进行比较的都是数值类型,即使他们的数据类型不同,只要是值相同,都将返回 true
1
2
3
int num = 2;
double num2 = 2.0;
System.out.println(num == num2); // true
但是如果比较的是引用类型,那么两个引用必须指向同一个对象,也就是引用类型比较的是两个变量指向的内存地址
equals Object.equals()方法默认实现就是返回两个对象 == 的比较结果(指向的内存地址)
但是很多对象重写了equals方法,比如String、Date、Integer等,那么比较是所指向对象的内容。这要根据对象是否重写equals判断
- equals在java中是逻辑相等
- hashCode相等的两个对象equals不一定为true,但是equals为true的对象,hashCode值必须相等
String 中的 equals 方法是如何重写的
源码解读:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public boolean equals(Object anObject) {
if (this == anObject) { // 如果指向的是同一块地址,那么直接返回true(同一个对象)
return true;
}
if (anObject instanceof String) { // 先判断对象类型
String anotherString = (String)anObject;
// 如果长度相等才进行比较
int n = value.length;
// 在String中value的定义private final char value[];
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 通过遍历比较每个下标元素是否相等,char基本类型,所以是区分大小写的
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
为什么要重写 equals、hashCode 方法
先看下Object类的equals方法的注释
1
2
3
4
* Note that it is generally necessary to override the {@code hashCode}
* method whenever this method is overridden, so as to maintain the
* general contract for the {@code hashCode} method, which states
* that equal objects must have equal hash codes.
其中规定相等的对象必须具有相等的hash值
重写equals、hashCode方法,和hash表的数据结构有关,特别是在使用hashMap的时候,因为java hashMap是通过链地址法+红黑树解决hash值冲突。即同一个hash值下,会挂多个不同对象。如果向hashMap插入自定义对象的时候,可能会出现预想不到的结果。为了使两个逻辑相等的对象拥有相同的hashCode值,有必要重写hashCode()方法,同时equals默认比较的是对象的地址,所以也需要重写
String s1 = new String(“abc”)、String s2 = “abc”、s1 == s2 。语句1在内存中创建了几个对象
内存区有三个,分别是栈、堆、常量池
new关键字创建对象,一定会在堆区创建一个新对象,然后在堆中再建立一个"abc"对象,并放到new String对象里面,这里共两个对象
String s2 = "abc"则会在常量池中开辟一个空间创建String类型的”abc”对象
所以一共创建了三个对象
String 为什么是不可变的,为什么这么设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
}
在Java中,如果一个对象在创建后,它的状态不能改变,那么我们就认为这个对象是不可变的,即对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变
误区:对象和对象的引用
1
2
String s = "abc";
s = "123";
s是对象的引用,”abc”才是对象,改变的仅仅是s指向的地址,而”abc”对象仍然存在
String内部其实是堆成员变量字符数组value的封装,同样也被final修饰,而且数组也是对象,但是可以通过反射修改value引用指向的数组,不建议这么做。通过源码可以看到replace、substring等方法,其实也都是返回一个新对象。
这样设计的目的跟重要的是为了安全性
请描述一下 static 关键字和 final 关键字的用法
static: 表示全局或者静态,可以修饰
- 属性
- 方法
- 块
- 内部类
- static修饰属性和方法:不被某个对象拥有,而是成为类对象,被该类对象所有的实例对象共享
- static修饰块:永远只会被调用一次,和对象创建个数无关(如果是实例块的话,创建一次,就被调用一次)。一个类可以创建多个静态块,且被顺序执行
1
2
3
4
5
6
7
8
static{
静态块
}
{
实例块
}
- static只可以修饰内部类(静态内部类)
final: 表示最终,可以修饰
- 类
- 属性
- 方法
- 形参
- 修饰类:抽象类、接口本身作用就是为了继承,所以这两个不能修饰。而一旦被final修饰的类,是不可以被继承的
- 修饰属性:必须赋初始值,即使没有初始值,那么在构造方法中必须被赋值,一旦赋值后不能被修改
- 修饰方法:子类不能重写
- 修饰形参:修饰形参后,方法中不能再被赋值
接口和抽象类的区别是什么
设计思想不同:
- 抽象类是自下而上的过程,是对类的抽象,通过继承的方式拥有某些相同特性;
- 接口是对某一行为的规范,是对行为的抽象,一个类可以实现多个接口拥有多种行为
继承是“是不是”的关系,接口是“有没有”的关系
用法不同:
- 接口:
- 接口中只能声明方法,属性,事件,索引器,抽象类中可以有方法的实现,也可以自定义非静态的类变量
- 接口不能包含字段、构造函数、静态成员或常量
- 接口中的方法会自动使用public abstract修饰
- 抽象类
- 抽象类可以提供某些方法的部分实现
- 抽象类的成员可以是私有的、受保护的、内部的或受保护的内部成员(其中受保护的内部成员只能在应用程序的代码或派生类中访问),接口成员被定义为公共的
重载和重写的区别
重载(Overload): 表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同,返回类型不能作为重载函数的区分标准)
重载是编译时多态,静态的,通过编译后变成不同的函数
重写(Override): 表示子类中的方法可以与父类中的某个方法的名称和参数完全相同
重写是运行时多态,通过动态绑定实现,是父类与子类之间的多态
面向对象的三大特性
面向对象的三大特性:封装、继承、多态
封装: 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
继承: 描述的是事物之间的所属关系,可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展
多态: 即一个引用变量到底指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须由程序运行期间才能决定(回忆下重载和重写) java实现多态有三个必要条件:继承、重写、向上转型
byte 的取值范围是多少、怎么计算出来的
byte类型在计算机中占据一个字节,也就是8 bit(位),最大值就是2^7 = 1111 1111
取值范围在 -128 ~ 127 ,一共是256
Java中用补码来表示二进制数,补码的最高位是符号位,最高位用 0 表示正数,最高位 1 表示负数,正数的补码就是其本身 ,由于最高位是符号位,所以正数表示的就是 0111 1111 ,也就是 127。最大负数就是 1111 1111,这其中会涉及到两个 0 ,一个 +0 ,一个 -0 ,+0 归为正数,也就是 0 ,-0 归为负数,也就是 -128,所以 byte 的范围就是 -128 - 127
HashMap 相关
底层数据结构
- JDK1.7: 数组 + 链表(拉链法解决哈希冲突)
- JDK1.8: 数组 + 链表 + 红黑树(链表长度 >= 8 且数组长度 >= 64 时转为红黑树,红黑树节点数 <= 6 时退化为链表)
重要参数
| 参数 | 默认值 | 说明 |
|---|---|---|
| initialCapacity | 16 | 初始容量,必须是2的幂次方 |
| loadFactor | 0.75f | 负载因子 |
| threshold | capacity * loadFactor | 扩容阈值 |
| TREEIFY_THRESHOLD | 8 | 链表转红黑树的阈值 |
| UNTREEIFY_THRESHOLD | 6 | 红黑树退化为链表的阈值 |
| MIN_TREEIFY_CAPACITY | 64 | 链表转红黑树时数组最小长度 |
hash计算
1
2
3
4
5
6
7
8
// JDK1.8的hash方法
static final int hash(Object key) {
int h;
// key为null时hash为0,所以HashMap允许key为null
// 高16位与低16位异或,增加低位的随机性,减少碰撞
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 定位桶下标:(n - 1) & hash,等价于 hash % n(n为2的幂次时)
put流程(JDK1.8)
- 对key求hash值,计算桶下标
(n-1) & hash - 如果桶为空,直接新建节点放入
- 如果桶不为空(发生碰撞):
- 如果第一个节点key相同,直接覆盖value
- 如果是TreeNode(红黑树),调用红黑树的插入方法
- 否则遍历链表,尾插法插入(JDK1.7是头插法),遍历中如果发现key相同则覆盖
- 链表插入后判断长度是否 >= 8,是则尝试转红黑树
- 判断
++size > threshold,是则扩容
扩容机制
- 容量变为原来的2倍,阈值也变为原来的2倍
- JDK1.8优化:扩容时不需要重新计算hash,只需看新增的那一位bit是0还是1
- 是0:位置不变
- 是1:新位置 = 原位置 + 旧容量
JDK1.7并发下的死循环问题
JDK1.7采用头插法,多线程扩容时可能导致链表形成环,后续get操作死循环。JDK1.8改为尾插法解决了此问题,但HashMap仍然是非线程安全的
HashMap vs Hashtable vs ConcurrentHashMap
| 对比项 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 否 | 是(synchronized) | 是 |
| null key/value | key可null,value可null | 都不可null | 都不可null |
| 初始容量 | 16 | 11 | 16 |
| 扩容方式 | 2倍 | 2倍+1 | 分段扩容 |
| 底层结构 | 数组+链表+红黑树 | 数组+链表 | JDK1.8: CAS+synchronized+数组+链表+红黑树 |
| 效率 | 高 | 低(全表锁) | 高(锁粒度细) |
ConcurrentHashMap的演进
- JDK1.7: Segment分段锁(继承ReentrantLock),每个Segment维护一段HashEntry数组,默认16个Segment,并发度16
- JDK1.8: 抛弃Segment,采用
CAS + synchronized锁住链表/红黑树的头节点,锁粒度更细,并发度更高
Integer 缓存池
先上JDK1.8源码,Integer缓存是Integer类中的静态内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Integer的valueOf方法
public static Integer valueOf(int i) {
// 先判断数值是否在-128 - 127之间
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
/**
* Cache to support the object identity semantics of autoboxing for values between (补充:jdk1.5支持自动装箱)
* -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=<size>} 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 不用创建新对象,而是直接从缓存池中取,减少堆中对象的分配
项目为 UTF-8 环境,char c = ‘中’,是否合法
合法的
Unicode 统一了所有字符的编码,是一个 Character Set,也就是字符集,字符集只是给所有的字符一个唯一编号,但是却没有规定如何存储,不同的字符其存储空间不一样,有的需要一个字节就能存储,有的则需要2、3、4个字节。UTF-8是Unicode的一种实现,”中”在UTF-8中占3个字节。但Java的char类型使用UTF-16编码,固定占2个字节,能表示Unicode基本多语言平面(BMP)中的所有字符,”中”的Unicode编码在BMP范围内,所以是合法的
Arrays.asList 获得的 List 使用时需要注意什么
Arrays.asList 转换完成后的 List 不能再进行结构化的修改,什么是结构化的修改?就是不能再进行任何 List 元素的增加或者减少的操作。这和Arrays的内部类ArrayList(不是java.util.ArrayList类)有关
1
2
3
4
5
6
7
8
9
10
/**
* @serial include
*/
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
// Arrays的内部类ArrayList继承AbstractList类
// 但是并没有重写add、remove方法
// AbstractList的add、remove方法是直接抛异常
}
Collection 和 Collections 区别
Collection 集合类的父类,是一个顶级接口。Collection 类只定义一些标准方法比如说 add、remove、set、equals 等,具体的方法由抽象类或者实现类去实现
Collections 集合类的工具类,Collections 提供了一些工具类的基本使用
fail-fast 和 fail-safe
快速失败和安全失败是对迭代器而言的
fail-fast(快速失败) 在 java.util 包的集合类就都是快速失败的,比如:HashMap、ArrayList
在使用迭代器对集合对象进行遍历的时候,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除、修改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值
每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedModCount 值,是的话就返回遍历;否则抛出异常,终止遍历
fail-safe(安全失败) java.util.concurrent 包下的类都是安全失败,比如:ConcurrentHashMap
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历
迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常
ArrayList、LinkedList 和 Vector 的区别
底层数据结构
- ArrayList: Object数组(连续内存空间)
- LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7改为非循环双向链表)
- Vector: Object数组(与ArrayList相同)
线程安全
- ArrayList: 非线程安全
- LinkedList: 非线程安全
- Vector: 线程安全,方法使用synchronized修饰
扩容机制
- ArrayList: 默认初始容量10,扩容为原来的1.5倍(
oldCapacity + (oldCapacity >> 1)) - Vector: 默认初始容量10,扩容为原来的2倍(也可通过构造函数指定增量
capacityIncrement) - LinkedList: 链表结构不存在扩容概念
性能对比
| 操作 | ArrayList | LinkedList | 原因 |
|---|---|---|---|
| 随机访问(get) | O(1) | O(n) | 数组支持下标直接访问,链表需要遍历 |
| 尾部插入(add) | O(1)均摊 | O(1) | ArrayList可能触发扩容 |
| 指定位置插入 | O(n) | O(n) | ArrayList需要移动元素,LinkedList需要先遍历定位 |
| 删除 | O(n) | O(n) | ArrayList需要移动元素,LinkedList需要先遍历定位 |
注:LinkedList虽然插入/删除理论是O(1),但定位到目标节点需要O(n),所以实际是O(n)
使用场景
- ArrayList: 读多写少的场景,频繁随机访问
- LinkedList: 频繁在头部/尾部插入删除(如队列、双端队列),LinkedList实现了Deque接口
- Vector: 不推荐使用,需要线程安全可用
Collections.synchronizedList()或CopyOnWriteArrayList
RandomAccess接口
ArrayList实现了RandomAccess接口(标记接口,无方法),表示支持快速随机访问。在Collections工具类的binarySearch方法中,会根据是否实现RandomAccess来选择不同的遍历方式:
1
2
3
4
5
6
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size() < BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key); // for循环下标访问
else
return Collections.iteratorBinarySearch(list, key); // 迭代器访问
}
Set 和 List 区别、Set 如何保证元素不重复
两个接口都是继承自Collection,是常用来存放数据项的集合
- 在List中允许插入重复的元素,而在Set中不允许重复元素存在
- 与元素先后存放顺序有关,List是有序集合,会保留元素插入时的顺序,Set是无序集合
- List可以通过下标来访问,而Set不能,set只能用迭代
Set如何保证元素不重复: 通过HashSet源码了解一下添加元素过程
1
2
3
4
5
6
7
//HashSet中map属性
private transient HashMap<E,Object> map;
//HastSet的add方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
通过上面可以看到HashSet是根据Map的特性来校验重复元素,再看一下HashMap的put方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) // 转红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
代码主要是通过hash计算key的位置,判断该位置是否有值,其中会通过hash和equals进行判断。这也是为什么用Map插入自定义对象的时候,需要重写equals和hashCode方法
Exception 和 Error 有什么区别
1
2
3
4
5
public class Exception extends Throwable{}
public class Error extends Throwable{}
public class Throwable implements Serializable{}
两个类都是继承Throwable类,Throwable 类是java语言中所有错误(errors)和异常(exceptions)的父类。只有继承于 Throwable 的类或者其子类才能够被抛出,还有一种方式是带有java中的 @throw 注解的类也可以抛出
Exception 泛指的是异常 ,Exception 主要分为两种异常,一种是编译期出现的异常,称为 checkedException 受检异常,一种是程序运行期间出现的异常,称为 uncheckedException非受检异常,也被统称为RuntimeException运行时异常。Exception 可以被捕获
- 常见的 checkedException 有 IOException
- 常见的 RuntimeException 主要有 NullPointerException(空指针) 、 IllegalArgumentException (非法参数)、 ArrayIndexOutofBoundException(数组越界) 、ClassCastException(类型转换异常)等
Error 是指程序运行过程中出现的错误,通常情况下会造成程序的崩溃,Error 通常是不可恢复的。绝大部分的Error都会导致程序(比如JVM自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError之类,都是Error的子类
String、StringBuffer 和 StringBuilder 有什么区别
String: JDK1.0,字符串常量,每次操作String字符串实际上是不断的创建新的对象,而原来的对象变成了垃圾被GC回收
StringBuffer: JDK1.0,是一个线程安全的容器,多线程场景下一般使用 StringBuffer 用作字符串的拼接
StringBuilder: JDK1.5,非线程安全的容器,StringBuilder 的 append 方法常用于字符串拼接,它的拼接效率要比 String 中 + 号的拼接效率高。StringBuilder 一般不用于并发环境
地址栏输入 URL 发生了什么
解析URL、DNS域名解析、浏览器与网站建立TCP连接、请求和数据传输、浏览器渲染页面