String s1 = new String("abc") 在内存中创建了几个对象
一个或者两个,String s1 是声明了一个 String 类型的 s1 变量,它不是对象。使用 new
关键字会在堆中创建一个对象,另外一个对象是 abc
,它会在常量池中创建,所以一共创建了两个对象;如果 abc 在常量池中已经存在的话,那么就会创建一个对象。
String 为什么是不可变的、jdk 源码中的 String 如何定义的、为什么这么设计。
首先了解一下什么是不可变对象
,不可变对象就是一经创建后,其对象的内部状态不能被修改,啥意思呢?也就是说不可变对象需要遵守下面几条原则
- 不可变对象的内部属性都是 final 的
- 不可变对象的内部属性都是 private 的
- 不可变对象不能提供任何可以修改内部状态的方法、setter 方法也不行
- 不可变对象不能被继承和扩展
与其说问 String 为什么是不可变的,不如说如何把 String 设计成不可变的。
String 类是一种对象,它是独立于 Java 基本数据类型而存在的,String 你可以把它理解为字符串的集合,String 被设计为 final 的,表示 String 对象一经创建后,它的值就不能再被修改,任何对 String 值进行修改的方法就是重新创建一个字符串。String 对象创建后会存在于运行时常量池中,运行时常量池是属于方法区的一部分,JDK1.7 后把它移到了堆中。
不可变对象不是真的不可变,可以通过反射
来对其内部的属性和值进行修改,不过一般我们不这样做。
static 关键字是干什么用的?谈谈你的理解
static 是 Java 中非常重要的关键字,static 表示的概念是 静态的
,在 Java 中,static 主要用来
- 修饰变量,static 修饰的变量称为
静态变量
、也称为类变量
,类变量属于类所有,对于不同的类来说,static 变量只有一份,static 修饰的变量位于方法区中;static 修饰的变量能够直接通过 类名.变量名 来进行访问,不用通过实例化类再进行使用。 - 修饰方法,static 修饰的方法被称为
静态方法
,静态方法能够直接通过 类名.方法名 来使用,在静态方法内部不能使用非静态属性和方法 - static 可以修饰代码块,主要分为两种,一种直接定义在类中,使用
static{}
,这种被称为静态代码块
,一种是在类中定义静态内部类
,使用static class xxx
来进行定义。 - static 可以用于静态导包,通过使用
import static xxx
来实现,这种方式一般不推荐使用 - static 可以和单例模式一起使用,通过双重检查锁来实现线程安全的单例模式。
final 关键字是干什么用的?谈谈你的理解
final 是 Java 中的关键字,它表示的意思是 不可变的
,在 Java 中,final 主要用来
- 修饰类,final 修饰的类不能被继承,不能被继承的意思就是不能使用
extends
来继承被 final 修饰的类。 - 修饰变量,final 修饰的变量不能被改写,不能被改写的意思有两种,对于基本数据类型来说,final 修饰的变量,其值不能被改变,final 修饰的对象,对象的引用不能被改变,但是对象内部的属性可以被修改。final 修饰的变量在某种程度上起到了
不可变
的效果,所以,可以用来保护只读数据,尤其是在并发编程中,因为明确的不能再为 final 变量进行赋值,有利于减少额外的同步开销。 - 修饰方法,final 修饰的方法不能被重写。
- final 修饰符和 Java 程序性能优化没有必然联系
抽象类和接口的区别是什么
抽象类和接口都是 Java 中的关键字,抽象类和接口中都允许进行方法的定义,而不用具体的方法实现。抽象类和接口都允许被继承,它们广泛的应用于 JDK 和框架的源码中,来实现多态和不同的设计模式。
不同点在于
抽象级别不同
:类、抽象类、接口其实是三种不同的抽象级别,抽象程度依次是 接口 > 抽象类 > 类。在接口中,只允许进行方法的定义,不允许有方法的实现,抽象类中可以进行方法的定义和实现;而类中只允许进行方法的实现,我说的方法的定义是不允许在方法后面出现{}
使用的关键字不同
:类使用class
来表示;抽象类使用abstract class
来表示;接口使用interface
来表示变量
:接口中定义的变量只能是公共的静态常量,抽象类中的变量是普通变量。
重写和重载的区别
在 Java 中,重写和重载都是对同一方法的不同表现形式,下面我们针对重写和重载做一下简单的区分
子父级关系不同
,重写是针对子级和父级的不同表现形式,而重载是在同一类中的不同表现形式;概念不同
,子类重写父类的方法一般使用@override
来表示;重写后的方法其方法的声明和参数类型、顺序必须要与父类完全一致;重载是针对同一类中概念,它要求重载的方法必须满足下面任何一个要求:方法参数的顺序,参数的个数,参数的类型任意一个保持不同即可。
byte的取值范围是多少,怎么计算出来的
byte 的取值范围是 -128 -> 127 之间,一共是 256 。一个 byte 类型在计算机中占据一个字节,那么就是 8 bit,所以最大就是 2^7 = 1111 1111。
Java 中用补码
来表示二进制数,补码的最高位是符号位,最高位用 0 表示正数,最高位 1 表示负数,正数的补码就是其本身
,由于最高位是符号位,所以正数表示的就是 0111 1111 ,也就是 127。最大负数就是 1111 1111,这其中会涉及到两个 0 ,一个 +0 ,一个 -0 ,+0 归为正数,也就是 0 ,-0 归为负数,也就是 -128,所以 byte 的范围就是 -128 - 127。
HashMap 和 HashTable 的区别
相同点
HashMap 和 HashTable 都是基于哈希表实现的,其内部每个元素都是 key-value
键值对,HashMap 和 HashTable 都实现了 Map、Cloneable、Serializable 接口。
不同点
- 父类不同:HashMap 继承了
AbstractMap
类,而 HashTable 继承了Dictionary
类
- 空值不同:HashMap 允许空的 key 和 value 值,HashTable 不允许空的 key 和 value 值。HashMap 会把 Null key 当做普通的 key 对待。不允许 null key 重复。
- 线程安全性:HashMap 不是线程安全的,如果多个外部操作同时修改 HashMap 的数据结构比如 add 或者是 delete,必须进行同步操作,仅仅对 key 或者 value 的修改不是改变数据结构的操作。可以选择构造线程安全的 Map 比如
Collections.synchronizedMap
或者是ConcurrentHashMap
。而 HashTable 本身就是线程安全的容器。 - 性能方面:虽然 HashMap 和 HashTable 都是基于
单链表
的,但是 HashMap 进行 put 或者 get 操作,可以达到常数时间的性能;而 HashTable 的 put 和 get 操作都是加了synchronized
锁的,所以效率很差。
- 初始容量不同:HashTable 的初始长度是11,之后每次扩充容量变为之前的 2n+1(n为上一次的长度)而 HashMap 的初始长度为16,之后每次扩充变为原来的两倍。创建时,如果给定了容量初始值,那么HashTable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。
HashMap 和 HashSet 的区别
HashSet 继承于 AbstractSet 接口,实现了 Set、Cloneable,、java.io.Serializable 接口。HashSet 不允许集合中出现重复的值。HashSet 底层其实就是 HashMap,所有对 HashSet 的操作其实就是对 HashMap 的操作。所以 HashSet 也不保证集合的顺序,也不是线程安全的容器。
HashMap 的底层结构
JDK1.7 中,HashMap 采用位桶 + 链表
的实现,即使用链表
来处理冲突,同一 hash 值的链表都存储在一个数组中。但是当位于一个桶中的元素较多,即 hash 值相等的元素较多时,通过 key 值依次查找的效率较低。
所以,与 JDK 1.7 相比,JDK 1.8 在底层结构方面做了一些改变,当每个桶中元素大于 8 的时候,会转变为红黑树,目的就是优化查询效率。
HashMap 的长度为什么是 2 的幂次方
这道题我想了几天,之前和群里小伙伴们探讨每日一题的时候,问他们为什么 length%hash == (n - 1) & hash,它们说相等的前提是 length 的长度 2 的幂次方,然后我回了一句难道 length 还能不是 2 的幂次方吗?其实是我没有搞懂因果关系,因为 HashMap 的长度是 2 的幂次方,所以使用余数来判断在桶中的下标。如果 length 的长度不是 2 的幂次方,小伙伴们可以举个例子来试试
例如长度为 9 时候,3 & (9-1) = 0,2 & (9-1) = 0 ,都在 0 上,碰撞了;
这样会增大 HashMap 碰撞的几率。
HashMap 多线程操作导致死循环问题
HashMap 不是一个线程安全的容器,在高并发场景下,应该使用 ConcurrentHashMap
,在多线程场景下使用 HashMap 会造成死循环问题(基于 JDK1.7),出现问题的位置在 rehash
处,也就是
do { Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了 int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null);
这是 JDK1.7 的 rehash 代码片段,在并发的场景下会形成环。
JDK1.8 也会造成死循环问题。
HashMap 线程安全的实现有哪些
因为 HashMap 不是一个线程安全的容器,所以并发场景下推荐使用 ConcurrentHashMap
,或者使用线程安全的 HashMap,使用 Collections
包下的线程安全的容器,比如说
Collections.synchronizedMap(new HashMap());
还可以使用 HashTable ,它也是线程安全的容器,基于 key-value 存储,经常用 HashMap 和 HashTable 做比较就是因为 HashTable 的数据结构和 HashMap 相同。
上面效率最高的就是 ConcurrentHashMap。
</div>