1、接口的意义-百度
规范、扩展、回调
2、抽象类的意义-百度
- 为其他子类提供一个公共的类型
- 封装子类中重复定义的内容
- 定义抽象方法,子类虽然有不同的实现,但是定义时一致的
3、内部类的作用-乐视
- 内部类可以使用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立;
- 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类;
- 创建内部类对象的时刻并不依赖于外围类对象的创建;
- 内部类没有令人疑惑的is-a关系,他就是一个独立的实体;
- 内部类提供了更好的封装,除了该外围类,其他类都不能访问;
4、Java 虚拟机的特性-百度-乐视
Java语言的重要特性是平台无关性,而使用Java虚拟机是实现这一特点的关键,一般的高级语言要想运行在不同的平台上,至少需要编译成不同的目标代码,而引入Java语言虚拟机后,Java语言在不同平台运行时不需要重新编译。Java语言屏蔽了和具体平台相关的信息,使Java语言编译程序只需生成在Java虚拟机上运行的目标码(字节码),就可以在多个平台不加修改的运行。Java虚拟机在执行字节码的时候再将字节码解释成具体平台的机器指令运行。
5、哪些情况下的对象会被垃圾回收机制处理掉-美团-小米
Java垃圾回收机制最基本的做法就是分代回收。内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在不同的世代区域中,一般实现的世代为3个:年轻、年老、永久。内存的分配是发生在年轻世代中的,当一个对象的存活时间足够长的时候,它就会被复制到年老的世代中。对于不同世代可以使用不同的回收算法,进行世代划分的出发点是对应用中对象存活时间进行研究后得出统计规律。一般来说,一个应用中大部分对象的存活时间都很短,比如局部变量的存活时间就只在方法的执行过程中,基于这一点,对于年轻世代的垃圾回收算法就可以很有针对性。
6、进程和线程的区别-猎豹-美团
一个程序至少有一个进程,一个进程至少有一个线程
- 线程的划分尺度小于进程,使得多线程程序的并发性更高
- 进程在执行的过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
- 线程在执行的过程中和进程有区别,每个独立的线程有一个程序运行的入口、顺序执行序列和程序出口,但是线程不能独立执行,必须依存于应用程序中,由应用程序提供多线程控制。
- 从逻辑的角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但是操作系统并不会将多个线程看做多个独立运行的应用程序来实现资源的调度和分配,这就是进程和线程的重要区别。
- 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
- 线程是进程的一个实体,是CPU调度和分配的基本单位,它是比进程更小的独立运行的基本单位。线程自己基本上不拥有系统资源,只是拥有一些在程序中必不可少的资源,比如程序计数器、寄存器、栈,但是它与同属一个进程的其他线程共享进程所拥有的其他资源。
- 一个线程可以创建和销毁另一个线程,同一个进程中的多个线程可以并发执行。
7、Java中==和equals和hashCode的区别-乐视
- ==是运算符,用于比较两个变量是否相等。
- equals,是Objec类的方法,用于比较两个对象是否相等。
- hashCode是获取字符串的哈希码,可以对字符串做粗略的比较,但是有极大的可能性会重复。
8、HashMap的实现原理-美团
- HashMap:是基于哈希表的map接口的非同步实现,此实现提供所有可选映射操作,并允许null值和null键。此类不保证映射的顺序,特别是它不保证该顺序永恒不变。
- HashMap的数据结构:在Java编程语言中,最基本的结构就是两种,一个是数组,另一个是模拟指针(引用),所有的数据结构都可以用着两种数据结构来构造,HashMap实际上是“链表散列”的数据结构,即数组和链表的结合体。
9、String-StringBuffer-StringBuilder区别-小米-乐视-百度
String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)
String类型和StringBuffer类型的主要区别在于,String的不可变的对象,因此在每次对String类型进行改变的时候都等同于生成了一个新的String对象,然后将指针指向了新的String对象,所以经常改变内容的字符串最好不要用String类型,这会导致JVM的GC开始工作,速度会相当慢。
而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。而在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的:
String S1 = "This is only a" +
"simple" + " test";
StringBuffer Sb = new StringBuffer("This is")
.append(" only a")
.append("simple")
.append("test");
- 你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个 String S1 = “This is only a” + “ simple” + “test”; 其实就是: String S1 = “This is only a simple test”; 所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如: String S2 = “This is only a”; String S3 = “ simple”; String S4 = “ test”; String S1 = S2 +S3 + S4; 这时候 JVM 会规规矩矩的按照原来的方式去做
- 在大部分情况下的效率都是 StringBuffer > String
- Java.lang.StringBuffer线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。
- 可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
- StringBuffer上的主要操作是append和insert,可以重载这些方法来接收任意类型的数据,每个方法都能高效的将给定数据转化成字符串,然后将该字符串的追加或者插入到字符串缓冲区中。append始终将字符串添加到缓冲区末端,insert方法将插入到指定位置。
- 在大部分情况下 StringBuilder > StringBuffer
- java.lang.StringBuilder一个可变的字符序列是Java5新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。
10、什么导致线程阻塞-58-美团
- 为了解决对共享存储区的访问冲突,Java引入了同步机制,现在考察多线程对共享资源的访问,显然同步机制已经不够,因为在任意时刻所要求的资源并不一定已经准备好了被访问,反过来,同一时间已经被准备好的资源也不一定是一个。为了解决这个问题,Java引入了对阻塞机制的支持。
- 阻塞指的是暂停一个线程的执行等待某个条件的发生。
- sleep()方法:指定以毫秒为单位的一段时间为参数,使得线程在指定时间内进入阻塞状态,不能得到CPU时间,指定的时间一过,线程重新进入可执行的状态。典型的,sleep()被用在等待某个资源就绪的情况下,测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止;
- suspend()和resume()方法:这两个方法要配套使用。suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume()被调用,才能使得线程重新进入可执行状态。典型的,suspend()和resume()被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
- yield()方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
- wait()和notify()方法:两个方法配套使用,wait()使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者将对应的notify()被调用或者超出指定时间线程重新进入可执行状态,后者则必须对应notify()调用。
初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。
上述的核心区别导致了一系列的细节上的区别
- 前面的所有方法都隶属于Thread类,但是wait()和notify()却隶属于Object类,也就是说,这是所有类都拥有的一对方法。因为这对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的wait()方法导致线程阻塞,并且该对象上的锁被释放。而调用任意对象的notify()方法则导致因调用该对象的wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
- 前面叙述的所有方法都可在任何位置调用,但是wait()和notify()必须在synchronized方法或块中调用,原因是只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的,调用这一对象上的锁必须为当前线程所拥有,这一才有锁可以释放。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException异常。
- wait() 和 notify() 方法的上述特性决定了它们经常和synchronized方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于block和wakeup原语(这一对方法均声明为synchronized)。它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。
- 调用notify() 方法导致解除阻塞的线程是从因调用该对象的wait()方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
- 除了notify(),还有一个方法notifyAll()也可起到类似作用,唯一的区别在于,调用notifyAll()方法将把因调用该对象的 wait()方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
11、多线程同步机制-猎豹
-
状态切换
多线程.png 线程间的状态转换
- 新建(new):新创建一个线程对象。
- 可运行(runnable):线程创建之后,其他线程(比如主线程)调用了该对象的start()方法,该状态的线程位于可运行的线程池当中,等待被线程调度选中,获取CPU使用权。
- 运行(running):可运行状态(runnable)的线程获得了CPU时间片(timeslice),执行程序代码。
- 阻塞(block):阻塞状态是指线程因为某种原因放弃CPU使用权,也让出CPU时间片,暂时停止运行,直到线程进入可运行状态(runnable),才有机会再次获得时间片,转到运行状态。线程阻塞的情况分为三种:
类型 | 原因 |
---|---|
等待阻塞 | 运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。 |
同步阻塞 | 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。 |
其他阻塞 | 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。 |
- 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
- 何时要同步
- 可见性同步的基本规则是在以下情况中必须同步:A, 读取上一次可能是由另一个线程写入的变量; B, 写入下一次可能由另一个线程读取的变量。
- 一致性同步:当修改多个相关值时,想要其它线程原子地看到这组更改——要么看到全部更改,要么什么也看不到。这适用于相关数据项(如粒子的位置和速率)和元数据项(如链表中包含的数据值和列表自身中的数据项的链)。
- 在某些情况中,不必用同步来将数据从一个线程传递到另一个,因为 JVM 已经隐含地为您执行同步。这些情况包括:A, 由静态初始化器(在静态字段上或 static{} 块中的初始化器); B, 初始化数据时; C, 访问 final 字段时; D, 在创建线程之前创建对象时; E, 线程可以看见它将要处理的对象时。
12、ArrayMap对比HashMap
在Java里面用Collection里面的HashMap作为容器我们使用的频率很高,而ArrayMap是Android api提供的一种用来提升特定场和内存使用率的特殊数据结构。
Java库里的HashMap其实是一个连续的链表数组,通过让key计算hash值后插入对应的index里。当hash值发生碰撞时,可以采用线性探测,二次hash,或者后面直接变成链表的结构来避免碰撞。因为hash的值不是连续的,所以hashmap实际需要占用的大小会比它实际能装的item的容量要大。我们可以看一下HashMap的源码:
public HashMap(int initialCapacity, float loadFactor)
{
// 初始容量不能为负数
if (initialCapacity < 0)
throw new IllegalArgumentException(
"Illegal initial capacity: " +
initialCapacity);
// 如果初始容量大于最大容量,让出示容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子必须大于 0 的数值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException(
loadFactor);
//....
// 设置容量极限等于容量 * 负载因子
threshold = (int)(capacity * loadFactor);
// 初始化 HashMap用于存储的数组
table = new Entry[capacity]; // ①
init();
}
你会发现它又一个变量叫loadfactor,还有threshold。threshold就是临界值的意思,代表当前HashMap的储存机构能容纳的最大容量,它等于loadfactor * 容量。当HashMap记录存入的item size大于threshold后,HashMap就会进行扩容(resize)。当我们第一次新建一个HashMap对象的时候,默认的容量是16,若你只打算在HashMap里放入3个元素那将浪费至少13个空间。
-
ArrayMap
ArrayMap是怎么实现节省内存的呢?先放数据结构图:
2.png
他用两个数组来模拟Map,第一个数组存放存放item的hash值,第二数组是把key,value连续的存放在数组里,通过先算hash在第一个数组里找到它的hash index,根据这个index在去第二个数组里找到这个key-value。
在这里,在第一个数组里查找hash index的方法当然是用二分查找啦(binary search)。
这个数据结构的设计就做到了,有多个item我就分配多少内存,做到了memory的节约。并且因为数据结构是通过数组组织的,所以遍历的时候可以用index直接遍历也是很方便的有没有!但是缺点也很明显,查找达不到HashMap O(1)的查找时间。
当要存储的对象较少的时候(1000以下的时候)可以考虑用ArrayMap来减少内存的占用。
13、HashMap和HashTable的区别-乐视-小米-360
- 继承和实现区别
Hashtable是基于陈旧的Dictionary类,完成了Map接口;HashMap是Java1.2引进的Map接口的一个实现(HashMap继承于AbstractMap,AbstractMap完成了Map接口)。 - 线程安全不同
HashTable的方法是同步的,HashMap是未同步,所以在多线程场合要手动同步HashMap。 - 对null的处理不同
HashTable不允许null值(key和value都不可以),HashMap允许null值(key和value都可以)。即 HashTable不允许null值其实在编译期不会有任何的不一样,会照样执行,只是在运行期的时候Hashtable中设置的话回出现空指针异常。 HashMap允许null值是指可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。 - 方法不同
HashTable有一个contains(Object value),功能和containsValue(Object value)功能一样。 - HashTable使用Enumeration,HashMap使用Iterator
- HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数
- 哈希值的使用不同
//HashTable直接使用对象的hashCode
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//HashMap重新计算hash值,而且用与代替求模:
int hash = hash(k);
int i = indexFor(hash, table.length);
static int hash(Object x) {
int h = x.hashCode();
h += ~(h << 9);
h ^= (h >>> 14);
h += (h << 4);
h ^= (h >>> 10);
return h;
}
static int indexFor(int h, int length) {
return h & (length-1);
}
14、容器类之间的区别-乐视-美团
Java容器类是java提供的工具包,包含了常用的数据结构:集合、链表、队列、栈、数组、映射等。从本文开始将开启一个系列详细分析Java容器中的每个成员,包括源代码分析,性能分析,不同容器之间对比等等,链接将同步更新在本文和置顶博文内。
Java容器主要可以划分为4个部分:List列表、Set集合、Map映射、工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)
15、抽象类和接口区别-360
默认的方法实现:
抽象类可以有默认的方法,也可以有抽象的方法;接口不存在方法的实现;实现:
子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现;子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现。构造方法:
抽象类可以有构造方法;接口不能有构造方法。与正常Java类的区别:
抽象类除了不能实例化之外,和普通Java类没有任何区别;接口是完全不同的类型。访问修饰符:
抽象方法可以有public、protected和default这些修饰符;接口方法默认修饰符是public,不可以使用其它修饰符。main方法:
抽象方法可以有main方法并且我们可以运行它;抽象方法可以有main方法并且我们可以运行它。多继承:
抽象类在java语言中所表示的是一种继承关系,一个子类只能存在一个父类,但是可以存在多个接口。速度:
抽象类比接口速度要快;接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。添加新方法:
如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码;如果你往接口中添加方法,那么你必须改变实现该接口的类。
15、什么是深拷贝和浅拷
- 浅拷贝:使用一个已知实例对新创建实例的成员变量逐个赋值,这个方式被称为浅拷贝。
- 深拷贝:当一个类的拷贝构造方法,不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值。这个方式称为深拷贝
16、对象锁和类锁是否会互相影响
对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
类锁: 对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。
Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。
类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。