Java面试题之Java基础

127 阅读17分钟

1.Java数据类型以及大小

boolean/1 byte/8 char/16 short/16 int/32 float/32 long/64 double/64

2.Java中有了基本类型为什么还要有包装类型(封装类型)

Java是一个面相对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。 另外,当需要往ArrayList,HashMap中放东西时,像int,double这种基本类型是放不进去的,因为容器都是装object的,这是就需要这些基本类型的包装器类了。

3.为什么分为基础数据类型和引用数据类型,String是不是?

Java 中规定了 String 不属于基本数据类型,只是代表一个类,属于引用类型

4.String

4.1 String为什么不可以修改?

String类是final类故不可以继承,也就意味着String引用的字符串内容是不能被修改。String有两种实例化方式: (1)直接赋值(例中,String str = "Hello";就是直接赋值实例化了) (2)使用new调用构造方法完成实例化;

4.2 String为什么是final?

1.为了实现字符串池 2.为了线程安全 3.为了实现String可以创建HashCode不可变性

4.3 那你知道final的作用呢?既然他的底层结构知道了,那你知道他的线程安全不?那你知道还有什么String类线程安全?

1.final可以修饰类,方法和变量, 2.final修饰的类,不能被继承,即它不能拥有自己的子类, 3.final修饰的方法,不能被重写, 4.final修饰的变量,无论是类属性、对象属性、形参还是局部变量,都需要进行初始化操作。

写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。

因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。

StringBuffer是线程安全的,StringBuilder是不安全的

4.4 这三者区别你知道不?(就是String、StringBuiler、StringBuffer),
  • 字符修改上的区别(主要) String:不可变字符串; StringBuffer:可变字符串、效率低、线程安全; StringBuilder:可变字符序列、效率高、线程不安全;

  • 初始化上的区别,String可以空赋值,后者不行,报错

4.5 String常用方法?

字符串长度length 字符串某一位置字符charAt 提取子串substring 字符串比较compareTo 字符串连接concat 字符串中单个字符查找indexOf 字符串中字符的大小写转换toLowerCase、toUpperCase 字符串中字符的替换replace

4.6 subString原理?

JDK1.6 在jdk 6 中,String类包含三个成员变量:char value[], int offset,int count,他们分别用来:存储真正的字符数组、存储数组的第一个位置索引、存储字符串中包含的字符个数。 当调用substring方法的时候,会创建一个新的string对象,但是这个string的值仍然指向堆中的同一个字符数组。这两个对象中只有count和offset 的值是不同的。 image.png

存在的问题 如果有一个很长的字符串,但是你只需要使用很短的一段,于是你使用substring进行切割,但是由于你实际上引用了整个字符串,这个很长的字符串无法被回收。往小了说,造成了存储空间的浪费,往大了说,可能造成内存泄漏。

JDK1.7 substring方法会在堆中创建一个新的数组。

image.png

5.==、equals区别

== 的作用:   基本类型:比较的就是值是否相同   引用类型:比较的就是地址值是否相同 equals 的作用:   引用类型:默认情况下,比较的是地址值。用来检测两个对象是否相等,即两个对象的内容是否相等。

6.集合类能用基本数据类型么,为什么?

集合中使用泛型,所以只能放如引用数据类型,不能放入基本数据类型

7. final 和 static 的区别

final: final可以修饰:属性,方法,类,局部变量(方法中的变量) final修饰的属性的初始化可以在编译期,也可以在运行期,初始化后不能被改变。 final修饰的属性跟具体对象有关,在运行期初始化的final属性,不同对象可以有不同的值。 final修饰的属性表明是一个常数(创建后不能被修改)。 final修饰的方法表示该方法在子类中不能被重写,final修饰的类表示该类不能被继承。

对于基本类型数据,final会将值变为一个常数(创建后不能被修改);但是对于对象句柄(亦可称作引用或者指针),final会将句柄变为一个常数(进行声明时,必须将句柄初始化到一个具体的对象。而且不能再将句柄指向另一个对象。但是,对象的本身是可以修改的。这一限制也适用于数组,数组也属于对象,数组本身也是可以修改的。方法参数中的final句柄,意味着在该方法内部,我们不能改变参数句柄指向的实际东西,也就是说在方法内部不能给形参句柄再另外赋值)。

static: static可以修饰:属性,方法,代码段,内部类(静态内部类或嵌套内部类) static修饰的属性的初始化在编译期(类加载的时候),初始化后能改变。 static修饰的属性所有对象都只有一个值。 static修饰的属性强调它们只有一个。 static修饰的属性、方法、代码段跟该类的具体对象无关,不创建对象也能调用static修饰的属性、方法等

8.常用的数据结构有哪些?并大说了一些操作的时间复杂度

image.png

9.介绍一下集合类

Map接口和Collection接口是所有集合框架的父接口:

  • Collection接口的子接口包括:Set接口和List接口
  • Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  • Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  • List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

10.list和set的区别?以及各个实现类和底层实现

两个接口都是继承自Collection,是常用来存放数据项的集合,主要区别如下: ① List和Set之间很重要的一个区别是是否允许重复元素的存在,在List中允许插入重复的元素,而在Set中不允许重复元素存在。 ② 与元素先后存放顺序有关,List是有序集合,会保留元素插入时的顺序,Set是无序集合。 ③ List可以通过下标来访问,而Set不能。

List常见实现类:  ArrayList(数组实现):允许对元素进行快速随机访问,从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

 Vector(数组实现):支持线程的同步,某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销(相关方法与ArrayList很相似,在方法上用synchronized修饰)。 发现当数组的大小不够的时候,需要重新建立数组,然后将元素拷贝到新的数组内,ArrayList(1.5倍 + 1)和Vector(2倍)的数组扩容的大小不同。

LinkedList(链表实现):很适合数据的动态插入和删除,随机访问和遍历速度比较慢。还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

Set接口: HashSet : 当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置(为什么HashSet 是如何保证不重复的)。也就是说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相等。不能保证元素的排列顺序,顺序有可能发生变化;集合元素可以是null,但只能放入一个null;

LinkedHashSet : LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起 来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。

TreeSet : TreeSet是SortedSet接口的唯一实现类,底层的数据结构是红黑树,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,

11.成员变量都是final修饰的话,如何进行赋值?

被final修饰的变量:三种赋值方式 在定义时直接赋值。 声明时不赋值,在constructor中赋值(最常用的方式) 声明时不赋值,在构造代码块中赋值

被final static修饰的变量:两种赋值方式 在定义时直接赋值. 在静态代码块里赋值

12.HashSet的底层实现

www.jianshu.com/p/1ed5fa8e3…

13.HashMap

13.1 HashMap数据结构结构

jdk1.7 数组+链表

jdk1.8 数组+链表+红黑树,如果链表长度大于等于8就会转化为红黑树,如果长度降至6红黑树会转化为链表。红黑树的出现解决了因为链表过长导致查询速度变慢的问题,因为链表的查询时间复杂度是O(n),而红黑树的查询时间复杂度是O(logn)。

13.2 hash冲突解决办法

根据对冲突的处理方式不同,哈希表有两种实现方式, (1)开放定址法 (2)链地址法 (3)再哈希法 (4)公共溢出区域法

jdk7 HashMap采用的是冲突链表方式。 jdk8中采用平衡树来替代链表存储冲突的元素,但hash() 方法原理相同

13.3 HashMap的hash函数讲一下

在HashMap存放元素时候有这样一段代码来处理哈希值,这是java 8的散列值扰动函数,用于优化散列效果;

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

为什么使用扰动函数? 理论上来说字符串的hashCode是一个int类型值,那可以直接作为数组下标了,且不会出现碰撞。但是这个hashCode的取值范围是[-2147483648, 2147483647],有将近40亿的长度,谁也不能把数组初始化的这么大,内存也是放不下的。 我们默认初始化的Map大小是16个长度 DEFAULT_INITIAL_CAPACITY = 1 << 4,所以获取的Hash值并不能直接作为下标使用,需要与数组长度进行取模运算得到一个下标值,也就是我们上面做的散列列子。 那么,hashMap源码这里不只是直接获取哈希值,还进行了一次扰动计算,(h = key.hashCode()) ^ (h >>> 16)。把哈希值右移16位,也就正好是自己长度的一半,之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性。计算方式如下图; image.png

说白了,使用扰动函数就是为了增加随机性,让数据元素更加均衡的散列,减少碰撞。

13.4 HashMap在什么条件下扩容?

如果bucket满了(超过load factor * current capacity),就要resize。 load factor为0.75,为了最大程度避免哈希冲突 current capacity为当前数组大小。

13.5 为什么扩容是2的次幂?

因为Hashmap计算存储位置时,使用了(n - 1) & hash。只有当容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突,所以扩容必须2倍就是为了维持容量始终为2的幂次方。

13.6 HashMap中put元素的过程是什么样么?

对key的hashCode()做hash运算,计算index; 如果没碰撞直接放到bucket里; 如果碰撞了,以链表的形式存在buckets后; 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树(JDK1.8中的改动); 如果节点已经存在就替换old value(保证key的唯一性),如果bucket满了(超过load factor*current capacity),就要resize。

13.7 知道HashMap中get元素的过程是什么样么?
  • 对key的hashCode()做hash运算,计算index;
  • 如果在bucket里的第一个节点里直接命中,则直接返回;
  • 如果有冲突,则通过key.equals(k)去查找对应的Entry;
    • 若为树,则在树中通过key.equals(k)查找,O(logn);
    • 若为链表,则在链表中通过key.equals(k)查找,O(n)。
13.8 jdk1.8中hashmap改了什么?
  • 由数组+链表的结构改为数组+链表+红黑树。
  • 优化了高位运算的hash算法:h^(h>>>16)
  • 扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。 最后一条是重点,因为最后一条的变动,hashmap在1.8中,不会在出现死循环问题。
13.9 为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。 当元素小于8个当时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于8个的时候,此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。 因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

13.10 不用红黑树,用二叉查找树可以么?

可以。但是二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。

13.11 当链表转为红黑树后,什么时候退化为链表?

为6的时候退转为链表。中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

13.12 HashMap是否线程安全?

非线程安全 JDK1.7 HashMap线程不安全体现在:死循环、数据丢失 JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。

JDK1.8 HashMap线程不安全体现在:数据覆盖 JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

13.13 HashMap如何解决线程不安全的问题?

使用Collections类的静态方法synchronizedMap获得线程安全的HashMap。 使用java.util.concurrent.concurentHashMap。

13.14 为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?

  之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

13.15 HashMap在JDK1.7和1.8除了数据结构的区别

(1)插入数据方式不同: JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7认为最新插入的应该会先被用到,所以用了头插法,但当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。 (2)扩容后数据存储位置的计算方式也不一样: 在JDK1.7的时候是重新计算数组下标 而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。 (3)扩容的条件不同,1.7需要容量超过阈值且发生hash冲突,1.8超过阈值即会扩容 (4)JDK1.7的时候是先进行扩容后进行插入,而在JDK1.8的时候则是先插入后进行扩容 (5)1.8中没有区分键为null的情况,而1.7版本中对于键为null的情况调用putForNullKey()方法。但是两个版本中如果键为null,那么调用hash()方法得到的都将是0,所以键为null的元素都始终位于哈希表table【0】中。 (6)jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容 (7)jdk1.7中的hash函数对哈希值的计算直接使用key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;