Java面试题总结
- 封装、继承、多态
- 1、Java中实现多态的机制是什么?
- 2、抽象类和接口的区别,如何选择?
- 3、重写和重载的区别?
- 4、是否可以继承String类?
- 5、构造器能否被重写?
- 6、public、private、protected的区别?
- 集合
- 1、列举几个Java中Collection类库中的常用类
- 2、List、Set、Map是否都继承自Collection?他们的存储特点分别是什么?
- 3、ArrayList、LinkedList、Vector之间有何区别和联系?
- 4、HashMap和HashTable、TreeMap和ConcurrentHashMap的区别?
- Collection和Collections的区别?
- 异常相关
- 1、Error和Exception的区别
- 2、异常的类型,什么是运行时异常
- 3、final、finally、finalize的区别
- 4、列举三个以上的RuntimeException
- 5、Java中的异常处理机制的简单原理和应用
- 其他
- 1、String和StringBulder、StringBuffer的区别
- 2、==和equals的区别
- 3、HashCode()的作用,和equals方法的关系
- 4、Inpute/OutputStream和Reader/Writter有什么区别
- 5、如何在字符流和字节流之间进行转换
- 6、switch可以使用哪些数据类型
- 7、Java的四种引用
- 8、序列化与反序列化
- 9、正则表达式
- 10、int和Integer的区别,什么是自动装箱和自动拆箱
- 11、Java8引入的Lambda表达式
- Java高级
- 1、进程和线程的区别
- 2、并行和并发的区别和联系
- 3、同步和异步
- 4、多线程的实现方式,有什么区别
- 5、什么是守护线程
- 6、如何停止一个线程?
- 7、什么是线程安全
- 8、synchronized和lock区别
- 9、当一个线程进入一个对象的一个synchronized方法后,其他线程是否可以进入此对象的其他方法?
- 10、启动一个线程使用run()还是start()?
- 11、wait和sleep的区别
- 12、Java中线程池的相关类
- JVM底层技术
- 1、gc的概念,如果A和B对象循环引用,是否可以被gc
- 2、jvm gc如何判断对象是否需要回收,有哪几种方式?
- 3、Java中能不能主动出发gc?
- 4、JVM的内存结构,堆和栈的区别
- 5、JVM堆的分代
- 6、Java中的内存溢出是什么,和内存泄漏有什么关系
- 7、Java的类加载机制,什么是双亲委派
- 8、ClassLoader的类加载方式
- IO
- 1、NIO、AIO和BIO之间的区别
- 2、IO和NIO常用用法
- 其他
- 1、hashcode有哪些算法
- 2、反射的基本概念,反射是否可以调用私有方法?
- 3、Java中泛型的基本概念
- 4、String s = new String("s"),创建了几个对象?
封装、继承、多态
1、Java中实现多态的机制是什么?
java特性:继承、封装、多态。
Java中实现多态的条件:继承、重写、向上转型
继承: 子承父业。
重写: 子类对父类中的某些方法进行重新定义,如重写equals方法。
向上转型: 将子类的引用赋给父类对象,这样该引用才具备同时调用父类方法和子类方法的资格。
实现形式: 继承(extends)和实现接口(implements)
静态多态和动态多态的区别?
答:多态分为编译时的多态和运行时的多态,前者是静态的,后者是动态的。
静态多态如方法的重载,在程序编译时期就已经确定了调用哪个函数;
而动态多态是在运行时才能动态地确定操作指针所指的对象,主要通过重写来实现。
java的多态机制遵循一个原则:当父类对象引用变量引用子类对象时,被引用的对象类型决定了调用谁的成员方法,而不是引用变量的类型去决定,但是这个被调用的方法必须是在超类中定义过的,也就是被子类覆盖的方法。
2、抽象类和接口的区别,如何选择?
抽象类
抽象类抽象出了一个实体的通用特性,通用性高,更抽象。有一个或多个抽象方法的类本身应该被声明是抽象的,除了抽象方法外,抽象类还可以包含具体的数据和具体的方法。抽象方法充当着站位的角色,他们的具体实现应该放在子类中,抽象类不能被实例化,如一个抽象类Person person = new Person();是错误的,但可以创建抽象类的对象变量,如Person person = new Student();其中student是person的实现类,Student类是非抽象类才可以。
接口
Java对接口给出的定义:接口不是类(不能使用new运算符实例化一个接口,但可以声明接口变量,接口变量必须引用实现了接口的类对象),而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。接口中所有的方法自动属于public,所以定义接口时不必提供关键字public!接口可以被扩展(继承其他接口)。接口中不能包含实例域或静态方法,但接口可以包含常量。接口是抽象方法的集合。如果一个类实现了某个接口,这个类就要实现接口的所有方法。
为什么要引入接口概念,而不直接使用抽象类呢?
Java不予许多重继承,但允许有多种实现。引入接口就避免了多重继承,也就避免了多重继承带来的复杂性和低效性。
抽象类与接口的对比
抽象类 | 接口 | |
实现 | 关键字extends。如果子类不是抽象类,它需要提供抽象类中所有的方法的实现 | 关键字implements。子类需要实现接口的所有方法 |
构造器 | 抽象类可以有构造器 | 接口没有构造器 |
访问修饰符 | public 、private、default都可以 | 方法默认标记为public,域标记为public static final |
多继承 | 抽象类可以继承一个类和实现多个接口 | 接口可以继承一个类或多个类 |
方法 | 抽象类可以在类本身中添加具体实现代码 | 接口方法内不能加入具体的代码实现 |
** 如何选择?**
抽象类和接口抽象出的东西的概念不一样,可以这样说:抽象类是对某一类事物的本质进行抽象,接口是对某一类事物的功能进行抽象。
抽象类表示这个对象是什么,接口表示这个对象能做什么。比如Person类抽象出这是人类,是个抽象类,人可以吃饭睡觉,其他动物也可以,于是就把吃饭睡觉抽象成接口,不同的动物去实现它。
所以当关注事物的本质的时候用抽象类,当关注一个操作的时候用接口。
3、重写和重载的区别?
重写(Override)
当某一子类继承了某一父类,这个子类继承了父类的所有方法,于是也可以对父类的方法进行重新编写,如重写toString()方法(你继承了你父亲的自然卷,你也可以将头发重新拉直,这是你的权利),使其继承下来的方法得到改变。需要注意的是子类重写的方法名,参数列表,返回类型必须跟父类相同,子类的访问权限修饰符不能小于父类的。
重载(Overload)
在同一个类中,同一个方法可以通过参数列表(参数类型、参数数量、参数顺序)不同则为重载,重载不关心返回类型是否相同,可同可不同,但不能由返回类型不同来判断为重载。
重载、重写与多态
重载和重写都是实现多态的方式,但区别在于重载是编译时多态,静态多态,即在编译时期就已经知道了该使用哪一个实现方法。重写是运行时多态,是动态多态,即在程序运行时才能知道引用对象具体使用的哪一个实现方法。
4、是否可以继承String类?
String类的定义为:public final calss String extends Object ,所以,String类是不可以被继承的,因为它被修饰为了final类。
final关键字
可以用来修饰变量、方法和类,用关键词final修饰的域成为最终域。用关键词final修饰的变量一旦赋值,就不能改变,也称为修饰的标识为常量。如果一个类的域被关键字final所修饰,它的取值在程序的整个执行过程中将不会改变。
假如说整个类都是final,就表明自己不希望从这个类继承,或者不答应其他任何人采取这种操作。换言之,出于这样或那样的原因,我们的类肯定不需要进行任何改变;或者出于安全方面的理由,我们不希望进行子类处理。
5、构造器能否被重写?
Constructor(构造器)不能被重写,但是可以被重载,如有参构造方法和无参构造方法。
6、public、private、protected的区别?
修饰符 | 当前类 | 同包 | 子类 | 其他包 |
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
集合
1、列举几个Java中Collection类库中的常用类
List、set、queue接口均继承与Collection接口,List接口又有ArrayList、LinkedList等实现类。Set接口又有HashSet、TreeSet等实现类。注意的是Map接口并不是Collection的子接口,而是同级的并列接口,Map有HashMap、HashTable等实现类。
2、List、Set、Map是否都继承自Collection?他们的存储特点分别是什么?
List和Set继承自Collection接口,但Map是“map”类的总接口,与Collection同级。
List特点:元素可重复存放,可以有序存放
Set特点:元素不可重复存放,无序存放
但TreeSet可以保持集合的有序性,支持自然排序和自定义排序
Set集合如何去重的
Map特点:以键值对key-value形式存放,如果键重复,那么后加入的值会覆盖之前的值。
3、ArrayList、LinkedList、Vector之间有何区别和联系?
ArrayList 是基于动态数组实现的
LinkedList 是基于链表实现的(准确来说应该是双向链表)
Vector 跟ArrayList实现方式差不多,但它在他的get、remove、set、size等操作方法上加了synchronized进行同步处理,理论上是线程安全的。但有些时候依然会出现同步问题(如抛出数组越界问题)synchronized的意思就是当一个线程访问某个方法时让其他线程不可以访问此方法,那为什么vector会出现同步问题呢?因为当同一个线程访问两个同步方法如get和remove方法时,删除了又去访问就会数组越界。
它们都继承自List接口。
增删改查四个方面比较ArrayList和LinkedList
增(插入新数据)(LinkedList优于ArrayList)
ArrayList是基于动态数组实现,当调用add方法增加发现容量不够时,会进行自动扩容。会创建一个更大的数组,然后将原数组中的数据拷贝至新数组中。
数组的增加操作可能会引起扩容操作,此时需要将原数组copy到新数组,再进行插入数据。若在数组中间插入数据,则需要将插入位置以后的数据分别向后移动一个单位。时间复杂度都挺高的(O(n)),若在数组末尾插入数据但不需要扩容时,插入效率高,时间快。
LinkedList是基于双向链表,在链表任何一个位置插入元素效率都很高,只需要将插入位置的相应头指针和尾指针变换一下指向就行了。时间复杂度为O(1)。
删除(LinkedList优于ArrayList)
ArrayList删除元素,需要将被删除元素后面的元素都向前移动一个单位,O(n)。如果删除元素后数组元素个数大大小于容量,还要进行缩容操作。
LinkedList 变换指针指向,O(1)。
查询元素(通常ArrayList优于LinkedList)
如果明确知道该元素位置,ArrayList查询速度最快。一个已经排好序的数组,查询某一个不知道下标位置的元素,可使用二分查找法。
LinkedList查找元素需要将链表遍历一遍,才能找到该元素,但双向链表可以使用头指针和尾指针同时开始向中间进行遍历。
修改元素
与查询情况类似。
4、HashMap和HashTable、TreeMap和ConcurrentHashMap的区别?
HashMap:1.7底层数组+链表,线程不安全,相比线程安全的Hashtable效率高,可以存储为null值的键或值。
Hashtable:跟HashMap差不多,但一些方法上加了synchronized,使之成为线程安全的。
TreeMap:线程不安全,底层红黑树实现,可以支持排序(默认、自定义)
ConcurrentHashMap:线程安全,效率较高,采用分段锁机制,将ConcurrentHashMap分成一个一个的segment(可重入锁)锁住,每个segment中包含一个hashEntry,并守护它,一个hashEntry是一个链表结构的元素,一个线程要修改hashEntry中的数据时,需要获得segment中的锁,由于锁是分段的,所以可以同时有多个线程操作不同的多个hashEntry,所以效率增高。
我对HashMap的理解 HashMap
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
底层数组+链表实现,数组元素Node实现了Map.Entry接口(在java8中是这样),里面有hash、key、value、next元素,有getKey、getValue等方法,key和value可以为null(HashTable无论是key还是value都不能为null)
public final int hashCode() {//hashCode方法
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}hashMap.entrySet(),返回的是Set集合,而Set集合里面存储的是Map.Entey实例,如
Set<Map.Entey<K,V>> set = hashMap.entrySet();
遍历Map集合:
Iterator<Map.Entry<K,V>> iter = set.iterator();
while(iter.hasNext()){
Map.Entry<K,V> entry = iter.next();
System.out.println(entry.getKey() + " " + entry.getValue())
}
HashMap线程不安全,HashTable线程安全,方式是在修改数据时锁住整个HashTable,所以效率低下,ConcurrentHashMap做了相关的优化,采用了分段锁机制,把集合分为多段落,用锁进行守护。
初始化HashMap时,初始size为1 << 4(16),最大为1 << 30(2^30),加载因子LoadFactor为0.75f,容量阈值threshold=Capacity * LoadFactor ,超过阈值扩容,扩容时:newsize = oldssize * 2,扩容后建立一张新表并将老表中的东西转移到新表中(见下文resize()方法),size无论什么时候都是2^n,即2的n次幂。
集合操作方法之put,向集合中添加元素(java7)
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key); //重点研究②
int i = indexFor(hash, table.length); //重点研究③
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
hash 推荐博客–nan/p/9014779.html
有hash就会涉及到hash冲突,常用的哈希函数的冲突解决办法中有一种方法叫做链地址法(Separate Chaining),其实就是将数组和链表组合在一起,发挥了两者的优势,我们可以将其理解为链表的数组。
final int hash(Object k) { //hash in java7
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
hash(Object k) 方法作用就是根据传入的k来定位其在HashMap中数组的位置。
indexFor(hash, table.length)
static int indexFor(int h, int length) { //indexFor in java7
// assert Integer.bitCount(length) == 1 : “length must be a non-zero power of 2”;
return h & (length-1);
}
indexFor()方法就是将hash()生成的整数转化为数组下标,操作只有一个,那就是
h & (length-1),ava之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快,位运算还可以解决负数的问题,因为Object类的hashCode()方法可以返回整数或者负数,但数组下标只能是正数,二进制中整型是32位,最高位是符号位,正数为0,负数为1,与00001111按位与,符号位为0仍然为0,符号为1变为0,不管怎样结果都为正数。
用&代替%,公式为:X % 2^n = X & (2^n – 1),所以,return h & (length-1);只要保证length的长度是2^n的话,就可以实现取模运算了。而HashMap中的length也确实是2的倍数,初始值是16,之后每次扩充为原来的2倍。
但是,单纯使用这种方法来确定位置仍然会引起冲突,而且冲突几率还挺大的。因为2^n-1转换为二进制为0000 1111,这种方式只是进行了二进制的低四位进行按位与,如果高为不同而低位相同的两个key也会产生冲突,如1111 1000、1010 1000两个数分别和0000 1111按位与的话结果都是一样的,都为8,即产生了hash冲突。那么如何解决呢?
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
方式就是上面这两行代码了。当程序对两个产生冲突hashcode进行一系列扰动计算之后,最终很好避免了hash冲突。 然后看上面的put()方法。。。。。。
java8put函数大致的思路为:
对key的hashCode()做hash,然后再计算桶的index;
如果没碰撞直接放到桶bucket里;
如果碰撞了,以链表的形式存在buckets后;
0ACITY,不进行转换而是进行resize操作)
如果节点已经存在就替换old value(保证key的唯一性)
如果表中实际元素个数超过阈值(超过load factor*current capacity),就要resize
在java8之前,每个数组的每个位置对应一个链表,java8之后,当hash冲突达到一定程度之后(阈值)就将链表转为红黑树。因为当冲突少时,其实链表更快,冲突多时,遍历链表的时间复杂度升高o(n),红黑树o(logn)
resize
哈希表结构是结合了数组和链表的优点,在最好情况下,查找和插入都维持了一个较小的时间复杂度O(1),不过结合HashMap的实现,考虑下面的情况,如果内部Entry[] tablet的容量很小,或者直接极端化为table长度为1的场景,那么全部的数据元素都会产生碰撞,这时候的哈希表成为一条单链表,查找和添加的时间复杂度变为O(N),失去了哈希表的意义。所以哈希表的操作中,内部数组的大小非常重要,必须保持一个平衡的数字,使得哈希碰撞不会太频繁,同时占用空间不会过大。
当目前的数组占用程度超过LoadFactor的时候,即thrashold>Capacity*LoadFactor;
HashMap就会发生resize,上面也说过,就是进行扩容,把bucket的大小扩充为之前的2倍,然后重新计算index,再把结点放到新的bucket中。
当容量从16扩展至32的时候,发生如下变化:
n-1 0000 0000 0000 1111 n-1 0000 0000 0001 1111
hash1 0000 0000 0000 0101 -----> hash1 0000 0000 0000 0101
hash2 0000 0000 0001 0101 hash2 0000 0000 0001 0101
bucket大小:n=2^k,变为两倍之后—>元素新位置:hash & (2(k+1)-1),而2(k+1)-1=2k+2k-1,即比原来的2^k-1高出1bit,所以就只需要看看高1bit的是0还是1,是0就位置不变,是1就原位置+16.新增的1bit可以认为是随机的,因此resize的过程可以均匀地把之前冲突的节点分散到新的bucket中。
如果只是在单线程中运行HashMap程序,不会出现问题,当我们提高程序性能进而使用多线程时,再使用HashMap就会出现死循环,CPU使用率会达到100%,重启程序后问题消失,隔一段时间问题又继续重演。
高并发下HashMap产生的环形链表
环形链表是在resize()这个步骤里产生的。
假设有一个HashMap进行某一次put之后需要扩容,线程A、B
同时进行操作。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next; //①线程A运行到这里被挂起
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; //这里是产生环形的关键地点
newTable[i] = e;
e = next;
}
}
}
Collection和Collections的区别?
Collection接口是集合的顶级接口,其子接口有List,Set,Queue
Collectios是集合类的一个工具类,它集成了许多集合类的方法,将之写成静态方法,供集合类使用,如
Collections.sort(),对集合内的元素进行自然排序。
Collections.shuffle(),混排,打乱集合内元素的顺序。
Collections.reverse();反转集合内的元素。
Collections.fill(list,int i),将集合内的所有元素以i替换。
Collections.copy(list1,list2);list1集合内的元素被list2内的元素挨个替换,list2长度如果大于list1,抛出异常。
Collections.min(list),返回Collections中最小元素(min) 。
Collections.max(list),返回Collections中最小元素(max) 。
int count = Collections.lastIndexOfSubList(list,li);返回指定源列表中最后一次出现指定目标列表的起始位置 。
int count = Collections.indexOfSubList(list,li);返回指定源列表中第一次出现指定目标列表的起始位置 。
Collections.rotate(list,-1); 根据指定的距离循环移动指定列表中的元素 ,如果是负数,则向左移动,正数则向右移动。
异常相关
1、Error和Exception的区别
Error和Exception都继承了Throwable类,在java中只有Throwable类型的实例才可以被抛出(throw)和捕获(catch),它是异常处理机制的基本组成类型。
Exception 是程序在运行过程中,可以预料的意外情况(如读取文件时文件可能不存在),这种情况应该被捕获然后进行相应的处理。
Error 是在正常情况下,不太可能会出现的情况,绝大部分Error都会导致程序处于非正常的、不可恢复状态。既然是非正常情况,所以我们不便于也不太需要捕获,比如OutOfMemoryError之类的,都是Error的子类。
延伸题目:
- NoClassDefFoundError 和 ClassNotFoundException区别
NoClassDefFoundError 是一个错误,产生原因在于:如果JVM或者ClassLoader尝试加载一个类的时候找不到类的定义,而这个类在编译期是存在的,运行时却找不到了,这时就会产生这个错误。原因可能是打包过程漏掉了部分类。
ClassNotFoundException 1.在使用Class.ForName方法来动态加载某一个类,任意一个类的类名如果被作为参数传递给这个方法都将导致该类被加载到JVM内存中,如果这个类在类路径中没有找到,抛出此异常。2.在使用反射方式加载某一个类时,类路径不存在这个类也会抛出此异常。 3.当一个类已经被某一个类加载器加载到内存中了,另一个类加载器再次尝试加载这个类。 - 下面代码中有什么不当之处
try{
//代码
Thread.sleep(1000);
}catch (Exception e){
//代码
}
捕获了Exception异常 我们的代码中应该避免捕获类似Exception的异常,而应该是捕获特定的异常,也就是说异常应该被扩散出来,以便于阅读,这里应该捕获InterruptedException。
生吞了异常 生吞异常很可能会导致难以诊断的诡异情况发生。难以让人知道是哪里发生了异常。
2、异常的类型,什么是运行时异常
Exception可分为检查时异常和运行时异常
检查时异常 在代码里面必须显式地进行捕获处理,这是编译器检查的一部分。(如IOException)
运行时异常 类似NullPointerException空指针异常、ArrayIndexOutOfBoundsException数组越界异常等,通常可以编码避免的逻辑错误,具体根据需要来判断是否进行捕获处理,编译期间不强制要求。
3、final、finally、finalize的区别
final 可以用来修饰类、方法、变量。
final修饰的类:不可以继承扩展。final修饰的方法不可以被Override。final修饰的变量不可以修改。如String类被修饰成final,表示不可以被继承,可以防止java标准类的功能被修改。
finally 是保证重点代码必须要被执行的一种机制。我们通常使用try-finally或try-catch-finally来关闭io连接、保证unlock锁等操作。
finalize 是基础类java.lang.Object的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize机制在JDK9开始被标记为deprecated(淘汰)。
4、列举三个以上的RuntimeException
类型转换、数组越界访问和试图访问空指针
5、Java中的异常处理机制的简单原理和应用
其他
1、String和StringBulder、StringBuffer的区别
String生成字符串常量
StringBulder和StringBuffer生成字符串变量
String str = "abc";
str = str + "de";
如上代码如果输出,则会输出abcde,当声明一个String类型的变量str时,jvm就分配给str一个内存空间,如果在str后面用“+”追加字符时,jvm就又重新开辟一个新的空间存储新生成的字符串str,之前的内存空间会在合适的时候会被gc掉。
特殊情况:如果是这样String str = "abc" + "def" ;
jvm会直接生成str = abcdef,而不是一个一个的生成。
所以当String类型的变量需要进行大量赋值或修改操作时,程序就会运行得非常慢,内存使用也会增加。此时应该使用StringBulder或者StringBuffer,使用.append()方法在字符串变量后面直接追加字符而不需要重新申请内存空间。
StringBulder和StringBuffer的区别
StringBulder 线程不安全,速度快
StringBuffer 线程安全,速度较StringBulder慢
在单线程多数据时使用StringBulder,因为没有同步cynchronized处理,构建字符串速度快;
在多线程多数据时使用StringBuffer,加入了cynchronized,保证其线程安全。
2、==和equals的区别
对于等号==
如果比的是基本数据类型(存储于栈中),则直接比较数据的“值”是否相等,等返回true,否则返回false
如果比较的是引用类型(存储于堆中),则比较的是对象内存地址是否相等。、
对于equals方法
equals来自Object类,所以equals方法只能用在类对象身上,而不能用在基本数据类型上。
对于没有重写equals方法的类,比较的是引用对象的内存地址。如果重写了equals方法(如String、Date等类),则趋于比较对象的内容。
3、HashCode()的作用,和equals方法的关系
HashCode()方法和equals()方法都在Object抽象类中定义。
HashCode()方法的作用就是将引用类型的物理地址转化为整数并返回。
一般重写equals()方法时就要重写hashCode()方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。不然会埋下许多潜在的BUG。
如果两个对象调用equals方法是相等的,那么他们两个任意一个调用hashCode方法就应该返回同样的整数结果。
如果两个对象调用equals方法是不相等的,那么整数结果不一定相同,即可同可不同。
如果象个对象调用hashCode方法返回两个相同的整数,则调用equals方法不一定返回true。
如果象个对象调用hashCode方法返回两个不相同的整数,则调用equals方法一定返回false。
Set集合去重原理:先调用引用的hashCode方法,不同,加入,同---->再调用equals方法,不同,加入,同则不能加入。
4、Inpute/OutputStream和Reader/Writter有什么区别
Reader和InputStream分别是I/O库提供的两套平行独立的等级机构,
InputStream、OutputStream是用来处理8位元(1字节)的流,
Reader、Writer是用来处理16位元(2字节)的流。
在这两种等级机构下,还有一道桥梁InputStreamReader、OutputStreamWriter负责进行InputStream到Reader的适配和由OutputStream到Writer的适配。
5、如何在字符流和字节流之间进行转换
InputStreamReader、OutputStreamWriter负责进行InputStream到Reader的适配和由OutputStream到Writer的适配。
6、switch可以使用哪些数据类型
java5之前,switch循环支持byte、short、char、int类型
java5,加入了枚举类以及byte、short、char、int类型的包装类,因为包装类可进行拆箱处理,对枚举类的支持是因为枚举类有一个ordinal方法,该方法实际上是一个int类型的数值。
java7,开始支持String类型。
switch case循环的的注意事项:
- case里面必须写break结束此case,否则程序会一直执行所有case直到寻找到一个break或default的出现。
- case条件里面只能是常量或者字面常量。
- default语句可有可无,但最多只有一个。
7、Java的四种引用
(回答出自《深入理解Java虚拟机》)
JDK1.2以前,Java中的引用定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表一个引用,这种定义很纯粹,很狭隘,一个对象在这种定义下只有被引用或没有被引用这两种状态。在描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,能保留在内存中;如果内存空间在进行垃圾回收之后还很紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的场景。
在JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用
强引用
强引用是指在程序代码中普遍存在的,类似“Object object = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用
软引用是用来形容一些还有用但非必需的对象。对于引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
弱引用
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联着的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否够用,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
虚引用
虚引用也称幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
8、序列化与反序列化
- 什么是序列化与反序列化
(1)Java序列化把Java对象转换为字节序列的过程,而反序列化是将字节序列恢复为Java对象的过程。
(2) 序列化 :对象序列化的最主要用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化就是将对象转换为有序的字节流,以便在网络上传输或保存到文件中。序列化后的字节流保存了对象的状态以及相关的描述信息。序列化机制的核心就是对象状态的保存和重建。
(3) 反序列化 对象的使用方从文件或者网络上获得序列化后的对象的字节流,根据字节流中所保存的对象的状态及描述信息通过反序列化重建对象。
(4) 本质上讲,序列化把对象就是通过一定的格式写入到有序的字节流中,反序列化就是从字节流中读取数据,恢复对象的原始状态。 - 为什么要使用序列化和反序列化呢(序列化与反序列化的好处)
(1)永久性的保存对象的状态,把对象的字节序列保存到本地文件或数据库中。
(2)通过序列化以字节流的形式可以使对象在网络上进行传输。
(3)通过序列化可以在进程之间传递对象。 - Java中如何实现序列化与反序列化
(1)Java类库中的API:
java.io.ObjectOutPutStream:表示对象输出流,它的writeObject方法可以对指定的对象进行序列化;
java.io.ObjectInPutStream:表示对象输入流,它的readObject方法可以读取字节序列,反序列化成为一个对象。
(2)实现序列化的要求
只有实现了Serializable或Externalizable接口的类对象才能被序列化,否则抛出java.io.NotSerializableException异常。
(3)序列化与反序列化实例
public class TestSerial {
public static void main(String[] args) throws IOException, ClassNotFoundException{
//序列化
FileOutputStream fos = new FileOutputStream("object.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
User user1 = new User("basevan", 18);
oos.writeObject(user1);
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("object.out");
ObjectInputStream ois = new ObjectInputStream(fis);
User user2 = (User) ois.readObject();
System.out.println("name:" + user2.getUserName()+ ",age:" + user2.getAge());
}
}
public class User implements Serializable {
//insert some code
}
9、正则表达式
10、int和Integer的区别,什么是自动装箱和自动拆箱
int是八个基本数据类型(byte,boolean,short,char,int,float,double,long)之一。java语言号称是一切都是面向对象,但原始数据类型是例外。
Integer 是int的包装类,它有一个int类型的字段存储数据,并且提供了基本的操作,如数学运算,int和字符串之间的转换。java5引入了传说中的自动装箱,自动拆箱,可以根据上下文自动进行基本数据类型和包装类的转换。
11、Java8引入的Lambda表达式
Java高级
1、进程和线程的区别
2、并行和并发的区别和联系
3、同步和异步
4、多线程的实现方式,有什么区别
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口,与runnable类似,但它可以返回一个对象
- 基于线程池的方式
5、什么是守护线程
Daemon线程,它是一种支持型线程,它主要用于程序中后台调度以及支持性工作。当一个java虚拟机中不存在非Daemon线程的时候,java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。
Daemon属性要在启动线程前设置,不能在启动后设置;
Daemon线程在java虚拟机退出时,它的finally块中的代码并不会执行。
6、如何停止一个线程?
- 程序正常运行结束
- 使用退出标志退出线程
一般run()方法执行完,线程就结束了,所以我们可以通过设置boolean类型的标志,通过控制标志的true或false来控制run()方法里面的while循环是否退出。 - interrupt方法结束线程
使用 interrupt()方法来中断线程有两种情况:
1.线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法
2.线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行 break 跳出循环
}
}
}
}
- stop()方法,但已经被淘汰的方法,避免使用。
7、什么是线程安全
当多个线程访问同一个类时,这个类始终都能保持正确的行为,就称这个类是线程安全的。
8、synchronized和lock区别
9、当一个线程进入一个对象的一个synchronized方法后,其他线程是否可以进入此对象的其他方法?
10、启动一个线程使用run()还是start()?
11、wait和sleep的区别
12、Java中线程池的相关类
JVM底层技术
1、gc的概念,如果A和B对象循环引用,是否可以被gc
2、jvm gc如何判断对象是否需要回收,有哪几种方式?
3、Java中能不能主动出发gc?
4、JVM的内存结构,堆和栈的区别
5、JVM堆的分代
6、Java中的内存溢出是什么,和内存泄漏有什么关系
7、Java的类加载机制,什么是双亲委派
8、ClassLoader的类加载方式
IO
1、NIO、AIO和BIO之间的区别
2、IO和NIO常用用法
其他
1、hashcode有哪些算法
2、反射的基本概念,反射是否可以调用私有方法?
3、Java中泛型的基本概念
4、String s = new String(“s”),创建了几个对象?