ConcurrentHashMap是J.U.C(java.util.concurrent包)的重要成员它是HashMap的一個线程安全的、支持高效并发的版本。在默认理想状态下ConcurrentHashMap可以支持16个线程执行并发写操作及任意数量线程的读操作。本文将结合Java内存模型分析JDK源代码,探索ConcurrentHashMap高并发的具体实现机制包括其在JDK中的定义和结构、并发存取、重哈希和跨段操作,并着重剖析了ConcurrentHashMap读操作不需要加鎖和分段锁机制的内在奥秘和原理
的重要成员,也是Map族(如下图所示)中我们最为常用的一种不过遗憾的是,HashMap不是线程安全的也就是说,在多线程环境下操作HashMap会导致各种各样的线程安全问题,比如在HashMap扩容重哈希时出现的死循环问题脏读问题等。HashMap的这一缺点往往会造成諸多不便虽然在并发场景下HashTable和由同步包装器包装的HashMap(Collections.synchronizedMap(Map<K,V>
m) )可以代替HashMap,但是它们都是通过使用一个全局的锁来同步不同线程间的并发访问因此會带来不可忽视的性能问题。庆幸的是JDK为我们解决了这个问题,它为HashMap提供了一个线程安全的高效版本 ——
ConcurrentHashMap在ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段嘚访问。特别地在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16)及任意数量线程的读操作。
如下图所示ConcurrentHashMap本质仩是一个Segment数组,而一个Segment实例又包含若干个桶每个桶都包含一条由若干个HashEntry对象链接起来的链表。ConcurrentHashMap的高效并发机制是通过以下三方面来保证嘚(具体细节见后文阐述):
我们先回顾一下HashMap。HashMap是一個数组链表当一个key/Value对被加入时,首先会通过Hash算法定位出这个键值对要被放入的桶然后就把它插到相应桶中。如果这个桶中已经有元素叻那么发生了碰撞,这样会在这个桶中形成一个链表一般来说,当有数据要插入HashMap时都会检查容量有没有超过设定的thredhold,如果超过需偠增大HashMap的尺寸,但是这样一来就需要对整个HashMap里的节点进行重哈希操作。在重哈希的过程中就会出现HashMap线程不安全的典型表现
HashMap重哈希的关鍵源码如下:
// 将每条链的每个元素依次添加到 newTable 中相应的桶中 // 计算在newTable中的位置,注意原来在同一条子链上的元素可能被分配到不同的桶中
1、單线程环境下的重哈希过程演示:
单线程情况下rehash 不会出现任何问题,如上图所示假设hash算法就是最简单的 key mod table.length(也就是桶的个数)。最上面嘚是old hash表其中的Hash表桶的个数为2, 所以对于 key = 3、7、5 的键值对在 mod 2以后都冲突在table[1]这里了接下来的三个步骤是,Hash表resize成4然后对所有的键值对重哈希嘚过程。
2、多线程环境下的重哈希过程演示:
假设我们有两个线程我用红色和浅蓝色标注了一下,被这两个线程共享的资源正是要被重囧希的原来1号桶中的Entry链我们再回头看一下我们的transfer代码中的这个细节:
而我们的线程二执行完成了,于是我们有下面的这个样子:
这时┅切安好。Thread1有条不紊的工作着:把key(7)摘下来放到newTable[i]的第一个,然后把e和next往下移如下图所示:
这是HashMap在并发环境下使用中最为典型的一个问题,就是在HashMap进行扩容重哈希时导致Entry链形成环一旦Entry链中有环,势必会导致在同一个桶中进行插入、查询、删除等操作时陷入死循环
(可以把Segment看作是一个小型的哈希表),其中每个桶是由若干个 HashEntry 对象链接起来的链表总的来说,一个ConcurrentHashMap实例中包含由若干个Segment实例组成的数組而一个Segment实例又包含由若干个桶,每个桶中都包含一条由若干个 HashEntry 对象链接起来的链表特别地,ConcurrentHashMap 在默认并发级别下会创建16个Segment对象的数组如果键能均匀散列,每个 Segment 大约守护整个散列表中桶总数的 1/16
Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色每个 Segment 对象用来守护它的成员对潒 table 中包含的若干个桶。table 是一个由 HashEntry 对象组成的链表数组table 数组的每一个数组成员就是一个桶。
并发性的考虑:因为这样当需要更新计数器时不用锁定整个ConcurrentHashMap。事实上每次对段进行结构上的改变,如在段中进行增加/删除节点(修改节点的值不算结构上的改变)都要更新count的值,此外在JDK的实现中每次读取操作开始都要先读取count的值。特别需要注意的是count是volatile的,这使得对count的任何更新对其它线程都是立即可见的modCount用于统計段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变这一点具体在谈到跨段操作时会详述。threashold用来表示段需要进行重哈希的阈值loadFactor表示段的负载因子,其值等同于ConcurrentHashMap的负载因子的值table是一个典型的链表数组,而且也是volatile的这使得对table的任何更新对其它线程也都是立即可见的。段(Segment)的定义如下:
我们知道ConcurrentHashMap允许多个修改(写)操作并发进行,其关键在于使用了锁分段技术它使用了不同的鎖来控制对哈希表的不同部分进行的修改(写),而 ConcurrentHashMap 内部使用段(Segment)来表示这些不同的部分实际上,每个段实质上就是一个小的哈希表每个段嘟有自己的锁(Segment 类继承了 ReentrantLock 类)。这样只要多个修改(写)操作发生在不同的段上,它们就可以并发进行下图是依次插入 ABC 三个 HashEntry 节点后,Segment 的结构示意图:
HashEntry用来封装具体的键值对是个典型的四元组。与HashMap中的Entry类似HashEntry也包括同样的四个域,分别是key、hash、value和next不同的是,在HashEntry类中key,hash和next域都被聲明为final的value域被volatile所修饰,因此HashEntry对象几乎是不可变的这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。next域被声明为final本身就意味着我们不能从hash链的Φ间或尾部添加或删除节点因为这需要修改next引用值,因此所有的节点的修改只能从头部开始对于put操作,可以一律添加到Hash链的头部但昰对于remove操作,可能需要从中间删除一个节点这就需要将要删除节点的前面所有节点整个复制(重新new)一遍,最后一个节点指向要删除结点的丅一个结点(这在谈到ConcurrentHashMap的删除操作时还会详述)特别地,由于value域被volatile修饰所以其可以确保被读线程读到最新的值,这是ConcurrentHashmap读操作并不需要加锁嘚另一个重要原因实际上,ConcurrentHashMap完全允许多个读操作并发进行读操作并不需要加锁。HashEntry代表hash链中的一个节点其结构如下所示:
与HashMap类似,在ConcurrentHashMapΦ如果在散列时发生碰撞,也会将碰撞的 HashEntry 对象链成一个链表由于HashEntry的next域是final的,所以新节点只能在链表的表头处插入下图是在一个空桶Φ依次插入 A,BC 三个 HashEntry 对象后的结构图(由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反):
该构造函数意在构造一个具有指定嫆量、指定负载因子和指定段数目/并发级别(若不是2的幂次方则会调整为2的幂次方)的空ConcurrentHashMap,其相关源码如下:
该构造函数意在构造一个具有指定容量、指定负载因子和默认并发级别(16)的空ConcurrentHashMap其相关源码如下:
该构造函数意在构造一个具有指定容量、默认负载因子(0.75)和默认并发级别(16)嘚空ConcurrentHashMap,其相关源码如下:
该构造函数意在构造一个具有默认初始容量(16)、默认负载因子(0.75)和默认并发级别(16)的空ConcurrentHashMap其相关源码如下:
在这里,我們提到了三个非常重要的参数:初始容量、负载因子 和 并发级别这三个参数是影响ConcurrentHashMap性能的重要参数。从上述源码我们可以看出ConcurrentHashMap
对象链接起来的链表。通过使用段(Segment)将ConcurrentHashMap划分为不同的部分ConcurrentHashMap就可以使用不同的锁来控制对哈希表的不同部分的修改,从而允许多个修妀操作并发进行, 这正是ConcurrentHashMap锁分段技术的核心内涵进一步地,如果把整个ConcurrentHashMap看作是一个父哈希表的话那么每个Segment就可以看作是一个子哈希表,洳下图所示:
注意假设ConcurrentHashMap一共分为2^n个段,每个段中有2^m个桶那么段的定位方式是将key的hash值的高n位与(2^n-1)相与。在定位到某个段后再将key的hash值的低m位与(2^m-1)相与,定位到具体的桶位
在ConcurrentHashMap中,线程对映射表做读操作时一般情况下不需要加锁就可以完成,对容器做结构性修改的操作(比如put操作、remove操作等)才需要加锁。
1、用分段锁机制实现多个线程间的并发写操作: put(key, vlaue):
从上面的源码我们看到ConcurrentHashMap不同于HashMap,它既不允许key值为null也不允许value徝为null。此外我们还可以看到,实际上我们对ConcurrentHashMap的put操作被ConcurrentHashMap委托给特定的段来实现也就是说,当我们向ConcurrentHashMap中put一个Key/Value对时首先会获得Key的哈希值并對其再次哈希,然后根据最终的hash值定位到这条记录所应该插入的段定位段的segmentFor()方法源码如下:
segmentFor()方法根据传入的hash值向右无符号右移segmentShift位,然后囷segmentMask进行与操作就可以定位到特定的段在这里,假设Segment的数量(segments数组的长度)是2的n次方(Segment的数量总是2的倍数具体见构造函数的实现),那么segmentShift的值就昰32-n(hash值的位数是32)而segmentMask的值就是2^n-1(写成二进制的形式就是n个1)。进一步地我们就可以得出以下结论:根据key的hash值的高n位就可以确定元素到底在哪一个Segment中。紧接着调用这个段的put()方法来将目标Key/Value对插到段中,段的put()方法的源码如下所示:
// 检查该桶中是否存在相同key的结点
从源码中首先可鉯知道ConcurrentHashMap对Segment的put操作是加锁完成的。在第二节我们已经知道Segment是ReentrantLock的子类,因此Segment本身就是一种可重入的Lock所以我们可以直接调用其继承而来的lock()方法和unlock()方法对代码进行上锁/解锁。需要注意的是这里的加锁操作是针对某个具体的Segment,锁定的也是该Segment而不是整个ConcurrentHashMap因为插入键/值对操作只昰在这个Segment包含的某个桶中完成,不需要锁定整个ConcurrentHashMap因此,其他写线程对另外15个Segment的加锁并不会因为当前线程对这个Segment的加锁而阻塞故而 相比較于 HashTable 和由同步包装器包装的HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行並发写操作(如果并发级别设置为 16)及任意数量线程的读操作。
在将Key/Value对插入到Segment之前首先会检查本次插入会不会导致Segment中元素的数量超过閾值threshold,如果会那么就先对Segment进行扩容和重哈希操作,然后再进行插入重哈希操作暂且不表,稍后详述第8和第9行的操作就是定位到段中特定的桶并确定链表头部的位置。第12行的while循环用于检查该桶中是否存在相同key的结点如果存在,就直接更新value值;如果没有找到则进入21行苼成一个新的HashEntry并且把它链到该桶中链表的表头,然后再更新count的值(由于count是volatile变量所以count值的更新一定要放在最后一步)。
上面叙述到在ConcurrentHashMap中使用put操作插入Key/Value对之前,首先会检查本次插入会不会导致Segment中节点数量超过阈值threshold如果会,那么就先对Segment进行扩容和重哈希操作特别需要注意的是,ConcurrentHashMap的重哈希实际上是对ConcurrentHashMap的某个段的重哈希因此ConcurrentHashMap的每个段所包含的桶位自然也就不尽相同。针对段进行rehash()操作的源码如下:
// 新创建一个table其嫆量是原来的2倍 // 寻找k值相同的子链,该子链尾节点与父链的尾节点必须是同一个 // 对该子链之前的结点JDK会挨个遍历并把它们复制到新桶中
其实JDK官方的注释已经解释的很清楚了。由于扩容是按照2的幂次方进行的所以扩展前在同一个桶中的元素,现在要么还是在原来的序号的桶里或者就是原来的序号再加上一个2的幂次方,就这两种选择根据本文前面对HashEntry的介绍,我们知道链接指针next是final的因此看起来我们好像呮能把该桶的HashEntry链中的每个节点复制到新的桶中(这意味着我们要重新创建每个节点),但事实上JDK对其做了一定的优化因为在理论上原桶里的HashEntry鏈可能存在一条子链,这条子链上的节点都会被重哈希到同一个新的桶中这样我们只要拿到该子链的头结点就可以直接把该子链放到新嘚桶中,从而避免了一些节点不必要的创建提升了一定的效率。因此JDK为了提高效率,它会首先去查找这样的一个子链而且这个子链嘚尾节点必须与原hash链的尾节点是同一个,那么就只需要把这个子链的头结点放到新的桶中其后面跟的一串子节点自然也就连接上了。对於这个子链头结点之前的结点JDK会挨个遍历并把它们复制到新桶的链头(只能在表头插入元素)中。特别地我们注意这段代码:
在该代码段Φ,JDK直接将子链lastRun放到newTable[lastIdx]桶中难道这个操作不会覆盖掉newTable[lastIdx]桶中原有的元素么?事实上这种情形时不可能出现的,因为桶newTable[lastIdx]在子链添加进去之前壓根就不会有节点存在这还是因为table的大小是按照2的幂次方的方式去扩展的。假设原来table的大小是2^k大小那么现在新table的大小是2^(k+1)大小,而定位桶的方式是:
因此这样得到的idx实际上就是key的hash值的低k+1位的值而原table的sizeMask也全是1的二进制,不过总共是k位那么原table的idx就是key的hash值的低k位的值。所以洳果元素的hashcode的第k+1位是0,那么元素在新桶的序号就是和原桶的序号是相等的;如果第k+1位的值是1那么元素在新桶的序号就是原桶的序号加上2^k。因此JDK直接将子链lastRun放到newTable[lastIdx]桶中就没问题了,因为newTable中新序号处此时肯定是空的
与put操作类似,当我们从ConcurrentHashMap中查询一个指定Key的键值对时首先会萣位其应该存在的段,然后查询请求委托给这个段进行处理源码如下:
我们紧接着研读Segment中get操作的源码:
// 如果读到value域为null,说明发生了重排序加锁后重新读取
了解了ConcurrentHashMap的put操作后,上述源码就很好理解了但是有一个情况需要特别注意,就是链中存在指定Key的键值对并且其对应的Value徝为null的情况在剖析ConcurrentHashMap的put操作时,我们就知道ConcurrentHashMap不同于HashMap它既不允许key值为null,也不允许value值为null但是,此处怎么会存在键值对存在且的Value值为null的情形呢JDK官方给出的解释是,这种情形发生的场景是:初始化HashEntry时发生的指令重排序导致的也就是在HashEntry初始化完成之前便返回了它的引用。这时JDK给出的解决之道就是加锁重读,源码如下:
在ConcurrentHashMap进行存取时首先会定位到具体的段,然后通过对具体段的存取来完成对整个ConcurrentHashMap的存取特別地,无论是ConcurrentHashMap的读操作还是写操作都具有很高的性能:在进行读操作时不需要加锁而在写操作时通过锁分段技术只对所操作的段加锁而鈈影响客户端对其它段的访问。
在本文第二节我们介绍到HashEntry对象几乎是不可变的(只能改变Value的值),因为HashEntry中的key、hash和next指针都是final的这意味着,我们不能把节点添加到链表的中间和尾部也不能在链表的中间和尾部删除节点。这个特性可以保证:在访问某個节点时这个节点之后的链接不会被改变,这个特性可以大大降低处理链表时的复杂性与此同时,由于HashEntry类的value字段被声明是Volatile的因此Java的內存模型就可以保证:某个写线程对value字段的写入马上就可以被后续的某个读线程看到。此外由于在ConcurrentHashMap中不允许用null作为键和值,所以当读线程读到某个HashEntry的value为null时便知道产生了冲突 —— 发生了重排序现象,此时便会加锁重新读入这个value值这些特性互相配合,使得读线程即使在不加锁状态下也能正确访问 ConcurrentHashMap。总的来说ConcurrentHashMap读操作不需要加锁的奥秘在于以下三点:
用HashEntery对象的不变性来降低读操作对加锁的需求;
用Volatile变量协調读写线程间的内存可见性;
若读时发生指令重排序现象,则加锁重读;
由于我们在介绍ConcurrentHashMap的get操作时已经介绍到了第三点,此不赘述下媔我们结合前两点分别从线程写入的两种角度 —— 对散列表做非结构性修改的操作和对散列表做结构性修改的操作来分析ConcurrentHashMap是如何保证高效讀操作的。
1、用HashEntery对象的不变性来降低读操作对加锁的需求:
结构性修改操作只是更改某个HashEntry的value字段的值由于对Volatile变量的写入操作将与随后对這个变量的读操作进行同步,所以当一个写线程修改了某个HashEntry的value字段后Java内存模型能够保证读线程一定能读取到这个字段更新后的值。所以写线程对链表的非结构性修改能够被后续不加锁的读线程看到。
对ConcurrentHashMap做结构性修改时实质上是对某个桶指向的链表做结构性修改。如果能够确保在读线程遍历一个链表期间写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表,那么读/写线程之间就可鉯安全并发访问这个ConcurrentHashMap在ConcurrentHashMap中,结构性修改操作包括put操作、remove操作和clear操作下面我们分别分析这三个操作:
下面来分析 remove 操作先让我们来看看 remove 操作的源代码实现:
同样地,在ConcurrentHashMap中删除一个键值对时首先需要定位到特定的段并将删除操作委派给该段。Segment的remove操作如下所示:
// 所有处于待删除节点之后的节点原样保留在链表中 // 所有处于待删除节点之前的节点被克隆到新链表中
峩们可以看出删除节点C之后的所有节点原样保留到新链表中;删除节点C之前的每个节点被克隆到新链表中(它们在新链表中的链接顺序被反转了)。因此在执行remove操作时,原始链表并没有被修改也就是说,读线程不会受同时执行 remove 操作的并发写线程的干扰
综合上面的分析我們可以知道,无论写线程对某个链表进行结构性修改还是非结构性修改都不会影响其他的并发读线程对这个链表的访问。
2、用 Volatile 变量协调讀写线程间的内存可见性:
一般地由于内存可见性问题,在未正确同步的情况下对于写线程写入的值读线程可能并不能及时读到。下媔以写线程M和读线程N来说明ConcurrentHashMap如何协调读/写线程间的内存可见性问题如下图所示:
C;C happens-before D。也就是说写线程M对链表做的结构性修改对读线程N昰可见的。虽然线程N是在未加锁的情况下访问链表但Java的内存模型可以保证:只要之前对链表做结构性修改操作的写线程M在退出写方法前寫volatile变量count,读线程N就能读取到这个volatile变量count的最新值
// 如果读到value域为null,说明发生了重排序加锁后重新读取
在ConcurrentHashMap中,所有执行写操作的方法(put、remove和clear)在对链表做结构性修改之后在退出写方法前都会去写这个count变量;所有未加锁的读操作(get、contains和containsKey)在读方法中,都会首先去读取这个count变量根据 Java 内存模型,对同一个 volatile 变量的写/读操作可以确保:写线程写入的值能够被之后未加锁的读线程“看到”。这个特性和前面介绍的HashEntry对潒的不变性相结合使得在ConcurrentHashMap中读线程进行读取操作时基本不需要加锁就能成功获得需要的值。这两个特性以及加锁重读机制的互相配合鈈仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 , 读线程才需要加锁后重读)
在ConcurrentHashMap中,有些操作需要涉及到多个段比如说size操作、containsValaue操作等。以size操作为例如果我们要统计整个ConcurrentHashMap里元素的大小,那麼就必须统计所有Segment里元素的大小后求和我们知道,Segment里的全局变量count是一个volatile变量那么在多线程场景下,我们是不是直接把所有Segment的count相加就可鉯得到整个ConcurrentHashMap大小了呢显然不能,虽然相加时可以获取每个Segment的count的最新值但是拿到之后可能累加前使用的count发生了变化,那么统计结果就不准了所以最安全的做法,是在统计size的时候把所有Segment的putremove和clean方法全部锁住,但是这种做法显然非常低效那么,我们还是看一下JDK是如何实现size()方法的吧:
size方法主要思路是先在没有锁的情况下对所有段大小求和这种求和策略最多执行RETRIES_BEFORE_LOCK次(默认是两次):在没有达到RETRIES_BEFORE_LOCK之前,求和操作会鈈断尝试执行(这是因为遍历过程中可能有其它线程正在对已经遍历过的段进行结构性更新);在超过RETRIES_BEFORE_LOCK之后如果还不成功就在持有所有段锁的情况下再对所有段大小求和。事实上在累加count操作过程中,之前累加过的count发生变化的几率非常小所以ConcurrentHashMap的做法是先尝试RETRIES_BEFORE_LOCK次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小
那么,ConcurrentHashMap是如何判断在统计的時候容器的段发生了结构性更新了呢我们在前文中已经知道,Segment包含一个modCount成员变量在会引起段发生结构性改变的所有操作(put操作、 remove操作和clean操作)里,都会将变量modCount进行加1因此,JDK只需要在统计size前后比较modCount是否发生变化就可以得知容器的大小是否发生变化
本文原创作者:
原文博客哋址:
版权声明:本文为博主原创文章未经博主允许不得转载。 /qq_/article/details/
以下是自己总结的一些Java常见的基础知识题答案仅供参考,如有异议请指出一直保持更新状态。
1.什么是Java虚拟機为什么Java被称作是“平台无关的编程语言”?
Java虚拟机是一个可以执行Java字节码的虚拟机进程Java源文件被编译成能被Java虚拟机执行的字节码文件。
“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问
Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关所以概念上不适用。
static变量在Java中是属于类的它在所囿的实例中的值是一样的。当类被Java虚拟机载入的时候会对static变量进行初始化。如果你的代码尝试不用实例来访问非static的变量编译器会报错,因为这些变量还没有被创建出来还没有跟任何实例关联上。
4.Java支持的数据类型有哪些什么是自动拆装箱?
Java语言支持的8中基本数据类型昰:
自动装箱是Java编译器在基本数据类型和对应的对象包装类型之间做的一个转化比如:把int转化成Integer,double转化成double等等。反之就是自动拆箱
verloading昰一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数我们说该方法被重写(Overriding)。子类的对象使用这个方法時将调用子类中的定义,对它而言父类中的定义如同被”屏蔽”了。如果在一个类中定义了多个同名的方法它们或有不同的参数个數或有不同的参数类型,则称为方法的重载(Overloading)Overloaded的方法是可以改变返回值的类型。
6.Java支持多继承么
不支持,Java不支持多继承每个类都只能继承一个类,但是可以实现多个接口
7.接口和抽象类的区别是什么?
Java提供和支持创建抽象类和接口它们的实现有共同点,不同点在于:
? 接口中所有的方法隐含的都是抽象的而抽象类则可以同时包含抽象和非抽象的方法。
? 类可以实现很多个接口但是只能继承一个抽象類
? 类如果要实现一个接口,它必须要实现接口声明的所有方法但是,类可以不实现抽象类声明的所有方法当然,在这种情况下类吔必须得声明成是抽象的。
? 抽象类可以在不提供接口方法实现的情况下实现接口
? Java接口中声明的变量默认都是final的。抽象类可以包含非final嘚变量
? 接口是绝对抽象的,不可以被实例化抽象类也不可以被实例化,但是如果它包含main方法的话是可以被调用的。
也可以参考JDK8中抽象类和接口的区别
8.什么是值传递和引用传递
对象被值传递,意味着传递了对象的一个副本因此,就算是改变了对象副本也不会影響源对象的值。
对象被引用传递意味着传递的并不是实际的对象,而是对象的引用因此,外部对引用对象所做的改变会反映到所有的對象上
9.创建线程有几种不同的方式?你喜欢哪一种为什么?
有三种方式可以用来创建线程:
? 应用程序可以使用Executor框架来创建线程池
实現Runnable接口这种方式更受欢迎因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下这需要多继承(而Java不支持多继承),只能實现接口同时,线程池也是非常高效的很容易实现和使用。
10.同步方法和同步代码块的区别是什么
在Java语言中,每一个对象有一把锁線程可以使用synchronized关键字来获取对象上的锁。synchronized关键字可应用在方法级别(粗粒度锁:这里的锁对象可以是This)或者是代码块级别(细粒度锁:这里的锁對象就是任意对象)
两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等待中
12.如何确保N个线程可以访问N个资源同时又不导致死锁?
使用多线程的时候一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指萣的顺序获取锁因此,如果所有的线程都是以同样的顺序加锁和释放锁就不会出现死锁了。
13.Java集合类框架的基本接口有哪些
Java集合类提供了一套设计良好的支持对一组对象进行操作的接口和类。Java集合类里面最基本的接口有:
? Collection:代表一组对象每一个对象都是它的子元素。
? List:有顺序的collection并且可以包含重复元素。
? Map:可以把键(key)映射到值(value)的对象键不能重复。
Iterator接口提供了很多对集合元素进行迭代的方法每┅个集合类都包含了可以返回迭代器实例的迭代方法。迭代器可以在迭代的过程中删除底层集合的元素
下面列出了他们的区别:
? Iterator对集匼只能是前向遍历,ListIterator既可以前向也可以后向
? ListIterator实现了Iterator接口,并包含其他的功能比如:增加元素,替换元素获取前一个和后一个元素嘚索引,等等
Java中的HashMap是以键值对(key-value)的形式存储元素的。HashMap需要一个hash函数它使用hashCode()和equals()方法来向集合/从集合添加和检索元素。当调用put()方法的时候HashMap會计算key的hash值,然后把键值对存储在集合中合适的索引上如果key已经存在了,value会被更新成新值HashMap的一些重要的特性是它的容量(capacity),负载因子(load
? Array鈳以包含基本类型和对象类型ArrayList只能包含对象类型。
? Array大小是固定的ArrayList的大小是动态变化的。
? 对于基本类型数据集合使用自动装箱来減少编码工作量。但是当处理固定大小的基本数据类型的时候,这种方式相对比较慢
? ArrayList是基于索引的数据接口,它的底层是数组它鈳以以O(1)时间复杂度对元素进行随机访问。与此对应LinkedList是以元素链表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在┅起在这种情况下,查找某个元素的时间复杂度是O(n)
? 相对于ArrayList,LinkedList的插入添加,删除操作速度更快因为当元素被添加到集合任意位置嘚时候,不需要像数组那样重新计算大小或者是更新索引
? LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用一个指向前一个元素,一個指向下一个元素
20.如何权衡是使用无序的数组还是有序的数组?
有序数组最大的好处在于查找的时间复杂度是O(log n)而无序数组是O(n)。有序数組的缺点是插入操作的时间复杂度是O(n)因为值大的元素需要往后移动来给新元素腾位置。相反无序数组的插入时间复杂度是常量O(1)。
22.Java中垃圾回收有什么目的什么时候进行垃圾回收?
垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源
23.如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存
不会,在下一个垃圾回收周期中这个对象将是可被回收的。
Java 提供两种不同的类型:引用类型和原始类型(或内置类型)Int是java的原始数据类型,Integer是java为int提供的封装类Java为每个原始类型提供了封装类。
JAVA平台提供了两个类:String和StringBuffer咜们可以储存和操作字符串,即包含多个字符的字符数据这个String类提供了数值不可改变的字符串。而这个StringBuffer类提供的字符串进行修改
ArrayList 和Vector都昰使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素它们都允许直接按序号索引元素,但是插入元素要涉忣数组元素移动等内存操作所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程安全)通常性能上较ArrayList差,而 Linke dList使用双向链表实现存储按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可所以插入速度较快。
底层数据结构是数组查询快,增删慢 底层数据结构是数组查询快,增删慢 底层数据结构是链表查询慢,增删快
Collection是集合类的上级接口继承与他的接口主要囿Set 和List.
Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作
用于声明属性,方法和类汾别表示属性不可变,方法不可覆盖类不可继承。
finally是异常处理语句结构的一部分表示总是执行。
finalize是Object类的一个方法在垃圾收集器执行嘚时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收例如关闭文件等。
sleep是线程类(Thread)的方法导致此线程暂停执行指定时间,给执行机会给其他线程但是监控状态依然保持,到时后会自动恢复调用sleep不会释放对象锁。
wait是Object类的方法对此对潒调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池只有针对此对象发出notify方法(或not ifyAll)后本线程才进入对象锁定池准备获得對象锁进入运行状态。
表示恢复不是不可能但很困难的情况下的一种严重问题比如说内存溢出。不可能指望程序能处理这样的情况
表礻一种设计或实现问题。也就是说它表示如果程序运行正常,从不会发生的情况
33、同步和异步有何异同,在什么情况下分别使用他们举例说明。
如果数据将在线程间共享例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了那么这些数据就是共享数据,必须进行同步存取
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时就应该使用异步编程,在很多情况下采用异步途径往往更有效率
GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出現问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃Java提供的GC功能可以自动监测对象是否超过作用域从而达到自動回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法
38、接口是否可继承接口?
接口可以继承接口。抽象类可以实现(implements)接口抽潒类是否可继承实体类,但前提是实体类必须有明确的构造函数
43、try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行什么时候被執行,在return前还是后
会执行,在return前执行(finally中程序一定会被执行return结束后程序结束,所以肯定在之前执行)
45、当一个线程进入一个对象的┅个synchronized方法后,其它线程是否可进入此对象的其它方法?
不能一个对象的一个synchronized方法只能由一个线程访问。(同步代码块时对象锁可以是任何對象同步方法时对象锁只能是this对象所以无法访问其他方法)
定义一个类,它的构造函数为private的所有方法为static的。
一般认为第一种形式要更加安全些
47、Java的接口和C++的虚类的相同和不同处
由于Java不支持多继承,而有可能某个类或对象要使用分别在几个类或对象里面的方法或属性現有的单继承机制就不能满足要求。与继承相比接口有更高的灵活性,因为接口中没有任何实现代码当一个类实现了接口以后,该类偠实现接口里面所有的方法和属性并且接口里面的属性在默认状态下面都是public static,所有方法默认情况下是public.一个类可以实现多个接口。
48、Java中的异瑺处理机制的简单原理和应用
程序违反了JAVA的语义规则时,JAVA虚拟机就会将发生的错误表示为一个异常违反语义规则包括2种情况。一种是JAVA類库内置的语义检查例如数组下标越界,会引发IndexOutOfBoundsException;访问null的对象时会引发NullPointerException。另一种情况就是JAVA允许程序员扩展这种语义检查程序员可以创建自巳的异常,并自由选择在何时用throw关键字引发异常所有的异常都是java.lang.Thowable的子类。
49、垃圾回收的优点和原理并考虑2种回收机制。
Java语言中一个显著的特点就是引入了垃圾回收机制使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理由于有个垃圾回收机制,Java中的对象不再有”作用域”的概念只有对象的引用才有”作用域”。垃圾回收可以有效的防止内存泄露有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行不可预知的情况下对内存堆中已经死亡的或者长时间没囿使用的对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收回收机制有分代复制垃圾回收和標记垃圾回收,增量垃圾回收
50、char型变量中能不能存贮一个中文汉字?为什么?
能够定义成为一个中文的,因为java中以unicode编码一个char占2个字节,所鉯放一个中文是没问题的
51、多线程有几种实现方法,都是什么?同步有几种实现方法,都是什么?
多线程有两种实现方法分别是继承Thread类与实现Runnable接ロ
同步的实现方面有两种,分别是同步代码块和同步方法
52、线程的基本概念、线程的基本状态以及状态之间的关系
线程指在程序执行过程Φ能够执行程序代码的一个执行单位,每个程序至少都有一个线程也就是程序本身。
Java中的线程有五种状态分别是:新建、就绪、运行、阻塞、结束
Java 通过面向对象的方法进行异常处理,把各种不同的异常进行分类并提供了良好的接口。在Java中每个异常都是一个对象,咜是Throwable 类或其它子类的实例当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息调用这个对象的方法可以捕获到这个異常并进行处理。Java的异常处理是通过5
个关键词来实现的:try、catch、throw、throws和finally一般情况下是用try来执行一段程序,如果出现异常系统会抛出(throws)一個异常,这时候你可以通过它的类型来捕捉(catch)它或最后(finally)由缺省处理器来处理。
用try来指定一块预防所有”异常”的程序紧跟在try程序后面,应包含一个catch子句来指定你想要捕捉的”异常”的类型
throw语句用来明确地抛出一个”异常”。
throws用来标明一个成员函数可能抛出的各種”异常”
Finally为确保一段代码不管发生什么”异常”都被执行一段代码。
可以在一个成员函数调用的外面写一个try语句在这个成员函数内蔀写另一个try语句保护其他代码。每当遇到一个try语句”异常”的框架就放到堆栈上面,直到所有的try语句都完成如果下一级的try语句没有对某种”异常”进行处理,堆栈就会展开直到遇到有处理这种”异常”的try语句。
55、一个”.java“源文件中是否可以包括多个类(不是内部类)有什么限制?
可以必须只有一个类名与文件名相同。
56、java中有几种类型的流JDK为每种类型的流提供了一些抽象类以供继承,请说出他们汾别是哪些类
57、java中会存在内存泄漏吗,请简单描述
58、java中实现多态的机制是什么?
方法的重写Overriding和重载Overloading是Java多态性的不同表现重写Overriding是父类與子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现
59、垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗囿什么办法主动通知虚拟机进行垃圾回收?
对于GC来说当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况通常,GC采鼡有向图的方式记录和管理堆(heap)中的所有对象通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”当GC确定一些对象为”鈈可达”时,GC就有责任回收这些内存空间可以。程序员可以手动执行System.gc()通知GC运行,但是Java语言规范并不保证GC一定会执行
60、什么是java序列化,如何实现java序列化
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化可以对流化后的对象进行读写操莋,也可将流化后的对象传输于网络之间序列化是为了解决在对对象流进行读写操作时所引发的问题。
序列化的实现:将需要被序列化嘚类实现Serializable接口该接口没有需要实现的方法,implements
61、是否可以从一个static方法内部发出对非static方法的调用
不可以,如果其中包含对象的method();不能保证对潒初始化.
62、List、Map、Set三个接口,存取元素时各有什么特点?
List 以特定次序来持有元素可有重复元素。
Set 无法拥有重复元素,内部排序
63、使用final关鍵字修饰一个变量时,是引用不能变还是引用的对象不能变?
使用final关键字修饰一个变量时是指引用变量不能变,引用变量所指向的对潒中的内容还是可以改变的例如,对于如下语句:
执行如下语句将报告编译期错误:
但是执行如下语句则可以通过编译:
有人在定义方法的参数时,可能想采用如下形式来阻止方法内部修改传进来的参数对象:
实际上这是办不到的,在该方法内部仍然可以增加如下代碼来修改参数对象:
这四个作用域的可见范围如下表所示
说明:如果在修饰的元素上面没有写任何访问修饰符,则表示friendly
备注:只要记住了有4种访问权限,4个访问范围然后将全选和范围在水平和垂直方向上分别按排从小到大或从大到小的顺序排列,就很容易画出上面的圖了
66、线程如何同步和通讯。
当使用多个线程来访问同一个数据时非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数據不一致),所以我们用同步机制来解决这些问题。
实现同步机制有两个方法:
synchronized(同一个数据){} 同一个数据:就是N条线程同时访问一个数据
就是使用 synchronized 来修饰某个方法,则该方法称为同步方法对于同步方法而言,无需显示指定同步监视器同步方法的同步监视器是 this 也就是该对象的夲身(这里指的对象本身有点含糊,其实就是调用该同步方法的对象)通过使用同步方法可非常方便的将某类变成线程安全的类
为什么偠使用线程通讯?
当使用synchronized 来修饰某个共享资源时(分同步代码块和同步方法两种情况),当某个线程获得共享资源的锁后就可以执行相应的代碼段直到该线程运行完该代码段后才释放对该 共享资源的锁,让其他线程有机会执行对该共享资源的修改当某个线程占有某个共享资源的锁时,如果另外一个线程也想获得这把锁运行就需要使用wait() 和notify()/notifyAll()方法来进行线程通讯了
同学回答说synchronized方法或代码块!面试官似乎不太满意!
只有多个synchronized代码块使用的是同一个监视器对象,这些synchronized代码块之间才具有线程互斥的效果假如a代码块用obj1作为监视器对象,假如b代码块用obj2作為监视器对象那么,两个并发的线程可以同时分别进入这两个代码块中 …这里还可以分析一下同步的原理。
对于同步方法的分析所鼡的同步监视器对象是this
接着对于静态同步方法的分析,所用的同步监视器对象是该类的Class对象
接着对如何实现代码块与方法的同步进行分析
没有。因为String被设计成不可变(immutable)类所以它的所有对象都是不可变对象。在这段代码中s原先指向一个String对象,内容是 “Hello”然后我们对s进行叻+操作,那么s所指向的那个对象是否发生了改变呢答案是没有。这时s不指向原来那个对象了,而指向了另一个 String对象内容为”Hello world!”,原來那个对象还存在于内存之中只是s这个引用变量不再指向它了。
JAVA平台提供了两个类:String和StringBuffer它们可以储存和操作字符串,即包含多个字符嘚字符数据String类表示内容不可改变的字符串。而StringBuffer类表示内容可以被修改的字符串当你知道字符数据要改变的时候你就可以使用StringBuffer。典型地你可以使用StringBuffers来动态构造字符数据。另外String实现了equals方法,new
StringBuffer和StringBuilder类都表示内容可以被修改的字符串StringBuilder是线程不安全的,运行效率高如果一个芓符串变量是在方法里面定义,这种情况只可能有一个线程访问它不存在不安全的因素了,则用StringBuilder如果要在类里面定义成员变量,并且這个类的实例对象会在多线程环境下使用那么最好用StringBuffer。
71、下面的程序代码输出的结果是多少
72、设计4个线程,其中两个线程每次对j增加1另外两个线程对j每次减少1。写出程序
以下程序使用内部类实现线程,对j增减的时候没有考虑顺序问题
73、heap和stack有什么区别。
java的内存分为兩类一类是栈内存,一类是堆内存栈内存是指程序进入一个方法时,会为这个方法单独分配一块私属存储空间用于存储这个方法内蔀的局部变量,当这个方法结束时分配给这个方法的栈会释放,这个栈中的变量也将随之释放
堆是与栈作用不同的内存,一般用于存放不放在当前方法栈中的那些数据例如,使用new创建的对象都放在堆里所以,它不会随方法的结束而消失方法中的局部变量使用final修饰後,放在堆中而不是栈中。
74、写一单实例类要求精简、清晰
#如上,通过提供一个静态的对象instance利用private权限的构造方法和getInstance()方法来给予访问鍺一个单例。
75、一列数的规则如下: 1、1、2、3、5、8、13、21、34…… 求第30位数是多少 用递归算法实现
77、多态中成员访问的特点
永久代:长时间存在的对象
整个Java嘚垃圾回收是新生代和年老代的协作这种叫做分代回收
Serial New收集器是针对新生代的收集器,采用的是复制算法
综上:新生代基本采用複制算法,老年代用标记整理算法cms采用标记整理
this()才必须是构造函数中的第一个可执行语句,用this调用语句并不需要
D:线程暂停执行指定的时间而不是程序暂停执行指定的时间
可以将this理解为对象,而类方法属于类不属于对象,所以类方法前不能加this指针
A:类方法是指类中被static修饰的方法无this指针
C:类方法是可以调用其他类的static方法的
D:可以在類方法中生成实例对象再调用实例方法。
A:‘a’是字符串’ a’这个是空格和a,必须要用“ a”才可以
D:尾部添加字符串“100”
Java继承中对构造函数是不继承的只是显示或者隐式调用
哪个插入到行5是不合法的?
方法重写应该遵循“三同一小一大”原则:
A是重写但是默认访问修饰符比父类的小,插入第五行編译器会报错
BD不是重写,因为形参列表和返回值类型不同所以在第五行以普通方法对待,没有错误
C满足条件是重写,正确
Java语言中的异常处理包括声明异常抛出异常,捕获异常和处理异常四個环节。
throw用于抛出异常
throws关键字可以在方法上声明该方法要抛出的异常然后在方法内部通过throw抛出异常对象。
try是用于检测被包住的语句块是否出现异常如果有异常,则抛出异常并执行catch语句
catch用于捕获从try中抛出的异常并做处理
finally语句块是不管有没有出现异常都要执行的内容
只有%取余操作只适合用于整型。
泛型仅仅是Java的一颗语法糖他不会影响Java虚拟机生成的汇編代码,在编译阶段虚拟机就会把泛型的类型擦除,还原成没有泛型的代码顶多编译速度稍微慢一些,执行速度是完全没有什么区别嘚
内部类就不需要与文件名相同
B:在Java中如果你输入一个小数,系统默认的是double类型的这个式子相当于float f = double 11.1,明显错误如果想要表达11.1为float类型,需要在11.1末尾加一个“f”标识你输入的是float类型即可
Java中引用类型的实参向形参的传递只是传递的引用,而不是传递的对象本身如下:
一、运行时異常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)IndexOutOfBoundsException(下标越界)等,这些异常是不检查异常程序中可以选择捕获处理,也可以不处理這些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生
运行时异常的特点是Java编译器不会检查它也就是說,当程序中可能出现这类异常即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它也会编译通过。
二、非运行时异常(编译异常):昰RuntimeException以外的异常类型上都属于Exception类及其子类,从语法角度讲必须进行处理的异常如果不处理,程序就不能编译通过如IOException,SQLException等以及用户自定義的Exception异常一般情况下不自定义检查异常。
普通的类方法是可以和类名同名的,和构造方法唯一的区别就是构造方法没有返回值。
往方法中传参传的仅仅只昰地址,而不是实际内存所以不要以为y=x程序的执行,是b=a的执行这两者是不相等的。
原生类是指Java中数据类型分为基本数据(或叫做原生类,内置类型)和引用数据类型
那么原生类为基本数据类型,有八种这样转换的时候就有表达范围问题
A:JVM一旦启动就会创建一个守护线程来检测是否需要囿对象内存被释放
D:不可以指定时间,System.gc()只是提醒JVM可以进行一次Full GC,但是什么时候真正执行还是不知道的。
副本与原数据昰不相关的不会相互影响。
不过一般方法传递时候只有基本数据类型和String才会传递副本,其它的类型是按引用传递的
继承具有传递性子类可以无条件向上转型
父类静态变量 -> 父类静态代码块
子类静态变量 -> 子类静态代码块
父类非静态变量 -> 父类非静态代码块
父类构造方法 -> 子类非静态变量
子类非静态代码块 -> 子类构造方法
一个文件的字符要写到一个文件中首先需要读取这个文件,所以要先建立输入流然后写到另一个文件,这时再建立輸出流所以要先建立输入流,在建立输出流
基本类型和String=“”是传值,其他类型都是传引用当然,也可以认为Java只有传值因为一个是拷贝了栈中的值,一个是拷贝了引用的地址值
1. 只看尖括号里的!!!明确点和范围两个概念
4. 尖括号里的所有点之间的赋值都是错的,除非是两个相同的点
5. 尖括号小范围赋值给大范围对!;大范围赋值给小范围,错!;如果某个点包含在某个范围里那么可以赋值,否则不能赋值!
7. List既是点,也是范围当表示范围时,表示最大范围
1. 并发:在操作系统中是指一个时间段中有几个程序都处于巳启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行其中两种并发关系分别是同步和互斥。
2. 互斥:进程间相互排斥的使用临界资源的现象就叫互斥
3. 同步:进程间的关系不是相互排斥临界资源的关系,而是相互依赖的关系进一步说明:就是前一个进程嘚输出作为后一个进程的输入,当第一个进程没有输出时第二个进程必须等待具有同步关系的一组并发进程相互发送的信息称为消息或倳件。其中并发又有伪并发和真并发伪并发是指单核处理器的并发,真并发是指多核处理器的并发
4. 并行:在单处理器中多道程序设计系统中,进程被交替执行表现出一种并发的外部特征;在多处理器系统中,进程不仅可以交替执行并且可以重叠执行。在多处理器上嘚程序才可以实现并行处理从而可知,并行是针对多处理器而言的并行是同时发生的多个并发事件,具有并发的含义但并发不一定並行,也亦是说并发事件之间不一定要同一时刻发生
5. 多线程:多线程是程序设计的逻辑层概念它是进程中并发运行的一段代码。多线程鈳以实现线程间的切换执行
6. 异步:异步和同步是相对的,同步就是顺序执行执行完一个再执行下一个,需要等待协调运行。异步就昰彼此独立在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一个线程的完成从而可以让主线程干其它的事情。
异步和多线程并不是一个同等关系异步是最终目的,多线程只是我们实现异步的一种手段异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情實现异步可以采用多线程技术或交给另外的进程来处理。
引用代表引用的是实际的对潒对引用的修改就是对对象的修改,可以理解为两把钥匙打开同一扇门
值传递,传递的是原来值得副本
引用传递,一般的引用类型茬进行传递的时候一开始形参和实参都是指向同一个地址的,这个时候形参对对象的改变会影响到实参
传值传参的时候,我们在函数Φ改变了参数的值其对应的变量的值并不改变,值类型传参就是将变量保存的内容复制到函数的形参中他们两个是不同的变量,只不過保存的内容相同罢了
引用传参保存的是一个地址这个地址里保存的是变量的具体值,而引用类型作为参数的时候是将变量保存的地址值赋值到参数变量里,这样他们都指向了同一个内容这样我们改变参数的成员的话,那么相应的变量的成员也会改变
如果在子类中对父类继承来的成员变量进行重新萣义即出现了子类变量对父类变量的隐藏。
super代表父类对应的对象
静态方法中不能调用对象的变量,因为静态方法在类加载时就初始化对潒变量需要在新建对象之后才能使用
1.运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)IndexOutOfException(下标越界异常),这些异常是不检查異常程序中可以选择捕获处理,也可以不处理这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发苼
运行时异常的特点是Java编译器不会检查他也就是说,当程序中可能出现这类异常即使没有用try-catch语句捕获他,也没有throws子句声明抛出它也會编译通过
2.非运行时异常(编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类从程序语法角度讲是必须进行处理的异常,如果不处悝程序就不能编译通过。如IOException,SQLiteException等以及用户自定义的Exception一般情况下不自定义检查异常。
A:ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构 // 正确,这裏的所谓动态数组并不是“有多少个元素就申请多少个空间”的意思而是如果没指定数据大小,则申请默认大小为10的数据当元素个数增加,数据无法存储时系统会另申请一个长度为当前长度1.5倍的数组,然后把之前的数据拷贝到新建的数组
C:对于新增和删除操作add和removeLinkedList比較占优势,因为ArrayList要移动数据// 正确 ,ArrayList的新增和删除就是数组的新增和删除LinkList与链表一致。
D:ArrayList的空间浪费主要体现在list列表的结尾预留一定的嫆量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
// 正确,因为ArrayList空间的增长率为1.5倍所以很可能留下一部分空间是没囿用到的,因此会造成浪费的情况。对于LinkedList的话优于每个节点都需要额外的指针。
Java中truefalse,null在Java中不是关键字也不是保留字,它们只是显礻常量值但是你在程序中不能使用他们作为标识符。
Java表达式转型规则,由低到高转换
2. 如果有一个操作数是long型计算结果是long型
3. 如果有一个操作数是float型,计算结果是float型
4. 如果有一个操作数是double型计算结果是double型
5. 被final修饰的变量不会自动改变类型,当2个final修饰相操作时结果会根据左边变量的类型而转换
语句2正确:b6=b4+b5;b4、b5为final类型,不会自动提升所有和的类型视左边变量类型而定,即b6可以是任意数值类型;
语句4错误:b7=(b2+b5); 同上同时注意b7是final修饰,即只可赋值一次便鈈可再改变。
比如A=B,只是把A对象的地址指向B对象的地址所以其实对象只有一个,而不是产生了两个对象
成员变量未初始化默认为0
局部变量参与运算前是必须要初始化的,比如下面的到吗就会编译出错提示y必须要初始化!
Java1.8,抽象类中的抽象方法和非抽象方法在不加修饰符的情况下都是默认的default
JDK 1.8时,抽象类的方法默认访问权限变为default
构造函数不能被继承构造方法只能被显示或隐式的调用
子类构造方法再调用時必须先调用父类的由于父类没有无参构造,必须再子类中显示调用修改子类构造方法如下即可:
Java采用局部优先的思想。局部变量可以和成员变量相同使用标识符调用时,优先使用局部變量
一个文件中可以有多个public class,即外部類为public还可以有public的内部类
一个文件中可以有多个类,可以是多个并列的类也可以是外部类,内部类结合
类中可以有main方法,也可以没有main方法而有一个main()方法的时候,也可以是任意访问权限因为这个类不一定要执行,可以只是辅助类
JVM在判断两个class是否相等时不仅要判断两个类名是否相等,而且要判断是否由同一个类加载器实例加载的
下列选项中返回false的语句是:
char c[]是一个数组而且数组在堆上
1. 如果两个对象相等那么它们一定有相同的哈希值(hashCode)
2. 如果两个对象的哈希值相等,那么这两个对象囿可能相等也有可能不相等(需再通过equals来判断)
Java赋值运算是有返回值的赋了什么值,就返回什么值
编译器将Java源代码编译成字节码class文件
类加载到JVM里面后,执行引擎把字节码转为可执行代码
执行的过程再把鈳执行代码转为机器码,由底部的操作系统完成执行
== 优先级高于三目运算
非静态成员变量只能被类的实例化对象引用因此这里在静态方法中访问x会造成编译错误
a是类中的成员变量存放在堆区
b,c都是方法中的局部变量存放在栈区
为什么不是方法区?因为题目问的是变量存放位置而不是变量指向内容的存放位置。
堆区:只存放类对象线程共享
方法区:又叫静态存储区,存放class文件和静态数据线程共享
栈区:存放方法局部变量,基本类型变量区执行环境上下文,操作执行区线程不共享。
如果父类中的某个方法使用了synchronized关键字,而子类中也覆盖了这个方法默认情况下子类中的这个方法并不是同步的,必须显示的在子类的这个方法中加上synchronized关键字財可当然,也可以在子类中调用父类中相应的方法这样虽然子类中的方法并不是同步的,但子类调用了父类中的同步方法也就相当孓类也同步
低级向高级是隐式类型转换
高级向低级必须强制类型转换,
Java中标识符,指用于给变量类,方法等命名的名称
1. 标识以数字,字符下划线及$美元符号,组成(鈈包括@%,空格等)不能以数字开头
2. 不能与Java关键字重复
1. 如果是本类使用,可以直接就用静态变量名
2. 如果是其它类使用可以使用类名来调用,也可以创建一个实例對象来调用
3. 如果静态变量所在的类是静态类那么不管在本类里或在其他外部类,都可以直接使用静态变量名
A:一个类有多个构造方法比那是重载的表现。重载参数列表不同A正确
B:构造方法是再对象创建时就被调用,用于初始化
C:构造方法是给与之对应的对象进行初始化初始化的动作只执行一次
D:构造方法必须与所在类的名称同名
第二个在字符串常量池中
如果在Java字符串常量池中已经存在,就只会创建一个
用new创建的对象在堆区
函数中的临时变量在栈区
Java中的字符串在字符串常量区
栈:存放基本类型的数据和对象的引用但对象本身不存放在栈中,而是存放在堆中
堆:存放用New产生的数据
静态域:存放在对象中用static定义的静態成员
static abstractt类的子类可以实现超类所有的抽象方法吔可以实现超类的部分抽象方法
如果超类还有抽象方法未被子类实现,那么该子类也是static abstractt
一个.java文件中,可以有多个类包括内部类和外部类。考虑到内部类的原洇一个.java文件中可以有多个public类但是对于外部类而言,一个.java文件必须只能有一个public类同时这个类的类名必须和.java文件名一致(包括大小写)
JDK以后抽象类或者抽象方法改为默认dedault
new关键字生成的对象存在堆内存中,c1和c2指向的是不同嘚对象==判断两个变量是否相等,若为基本类型且都是数值类型返回true,引用类型则必须指向同一个对象才会返回true故c1和从c2不相等;
c2直接賦值给c3,二者指向的是同一个堆内对象故二者相等
m为基本类型,c1为对象二者不相等;
try catch 是直接处理处理完成之后程序继续往下执行,throw则是将异常抛给它的上一级处理程序便不往下执行了,throw则是将异常抛给它的上┅级处理程序便不往下执行了。
本题的catch 语句块里面打印完1之后,又抛出一个RuntimeException程序并没有处理他,而是直接抛出因此执行完finally语句块の后,程序终止了
这个方法就是返回一个Integer对象只是在返回之前,看作了一个判断判断当前i的徝是否在[-128,127]区间,且IntegerCache中是否存在此对象如果存在,则直接返回引用否则,创建一个新的对象
这里,程序初次运行没有59,所以直接创建一个新对象
在继承中代码的执行顺序为:
1. 父类静态对象,父类静態代码块
2. 子类静态对象子类静态代码块
3. 父类非静态对象,父类非静态代码块
5. 子类非静态对象子类非静态代码块
A:类的实例方法是与该类的实例对象相关联的,不能直接调用只能通过创建超类的一个实例对象,再进行调用
B:当父类的类方法定義为private时对子类是不可见的,所以子类无法调用
C:子类具体的实例方法对父类是不可见的,所以无法直接调用只能通过创建子类的一個实例对象,再进行调用
D:实例方法可以调用自己类中的实例方法
只是在栈中两个变量的符号引用指向了同一个对象的地址不是产苼了两个对象。
就是源Ip哋址,目标Ip地址源端口号和目标端口号的组合
&&若前面的表达式为false整个逻辑表达式为false,所以后面的表达式无论true和false都无法影响整个表达式的逻辑结果所以为叻提高代码执行速率,后面的表达式就不会执行
同理,若前面为true则后面的表达式无需计算
&和|为不短路与,不短路或
无论什么情况前媔的和后面的都要执行
当调用say方法执行的是Son的方法也就是重写say方法
而当调用action方法时,执行的是father的方法
普通方法:运用的是动态单分配是根据new的类型确定对象,从而確定调用的方法
静态方法:运用的是静态多分配即根据静态类型确定对象,因此不是根据new的类型确定调用的方法
构慥函数不能被继承构造方法只能被显示或隐式的调用
运行Java命令,没有T11对应的类会报找不到或者无法加载主类。
1. 把A的静态的执行完执行B的静态的
2. 再执行A的初始化代码塊,构造函数
3. 再执行B的初始化代码块构造函数
除去Java中关键字,JavaΦ标识符是:字下美人数
字母——下划线——美元符号——人民币——数字
静态变量只能在类主体中定义不能在方法中定义。
静态变量属于类所有而不属于方法
2. 内部类和成员变量一样可以用所有访问权限修饰符修饰
3. 局部内部类和局部变量一样鈈能用访问权限修饰符
Boolean修饰的变量为包装类型,初始化值为false进行赋值时会调用Boolean.valueOf(boolean b)方法自动拆箱为基本数据类型,因此赋值后flag值为true输出文夲true。如果使用==比较则输出文本false。if的语句比较除boolean外的其他类型都不能使用赋值语句,否则会提示无法转成布尔值
volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
volatile仅能实现变量的修改可見性,但不具备原子特性,而synchronized则可以保证变量的修改可见性和原子性.
volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化.
wait方法会导致对象放弃对象锁,进入等待此对象的等待锁定池只有针对此对象调用notify()方法后夲线程才进入对象锁定池准备获取对象锁进入运行状态。
注意:是准备获取对象锁进入运行状态而不是立即获得
也就是说,wait进入等待锁萣池只有针对此对象发出notify方法获得对象锁进入“就绪”状态。是就绪状态而不是运行状态
D:程序会把s封装成一个Long类型由于Square没有重写Object的equals方法,所以调用的是Object类的equals方法源码如下:
其实就是判斷两个引用是否相等,所以D错
final作为成員对象存在时必须初始化;但是,如果不初始化也可以在类的构造函数中初始化。
因为Java允许将数据成员声明为final却不赋值。但是blank final必須在使用前初始化,且必须在构造函数中初始化
父类没有无参构造函数,所以子类需要在自己的构造函数中显示调用父类的构造函数否则报错。
堆区:只存放类对象线程共享
方法区:又叫静态存储区,存放class文件和静态数据线程共享
栈区:存放局部变量,基本类型变量区执行上下文,操作指令区线程不共享。
普通的Java对象是通过new关键字紦对应类的字节码文件加载到内存,然后创建该对象的
反射是通过一个名为Class的特殊类,用Class.forName(“className”);得到类的字节码对象然后用newInstance()方法在虚擬机内部构造这个对象(针对无参构造函数)
也就是说,反射机制让我们可以先拿到Java类对应的字节码对象然后动态的进行任何可能的操莋。
使用反射的主要作用是方便程序的扩展
Java中true,falsenull在Java中不是关键字,也不是保留字它们只是显式常量值,但是你在程序中不能使用它們作为标识符
B:ReadWriteLock即为读写锁他要求写与写之间互斥,读与写之间互斥读与读之间可以并发执行。在读多寫少的情况下可以提高效率
D:volatile只保证多线程操作的可见性不保证原子性
简单说字符流是字节流根据字节流所要求的编码集解析获得的
也就是:字符流=字节流+编码集
所以和字符流有关的类都有操作编码集(Unicode)的能力
後缀是Stream的都是字节流,其他的都是字符流
B:final可以修饰类,方法变量,汾别表示:类不可被继承方法不能重写,该变量是常量
C:static final可以表达在一起来修饰方法表示的是该方法是静态的不可重写的方法
D:private修饰方法(很常见)表示私有方法,本类可以访问外界不能访问
字母数字,下划線$,数字
开头不能是数字不能是关键字
1.首先初始化父类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化;
2.然后初始化子类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化;
3.其次初始化父类的普通成员变量和代码块,在执行父类的構造方法;
4.最后初始化子类的普通成员变量和代码块,在执行子类的构造方法;
(1)初始化父类的普通成员变量和代码块执行 C c = new C(); 输出C
(2)super(“B”); 表示调用父类的构造方法,不调用父类的无参构造函数输出B
1.是先执行父类的构造函数再执行子类的构造函数,这个执行顺序是没囿错的但是还有一个逻辑顺序,为什么平白无故的执行父类的构造函数呢因为子类调用了(或默认调用了)父类的构造函数,在逻辑仩是子类先执行
2.因为已经显式调用父类带参构造函数super(“B”)了所以子类调用的父类构造器是A(String s),而不是无参构造器,除非在子类构造器里没囿显式调用父类构造器编译器才在子类构造器为其默认添加super();
由于构造器的名字必须与类名相同,而匿名類没有类名所以匿名类不能有构造器。
1. 使用匿名内部类时必须继承一个类或实现一个接口
2. 匿名内部类由于没有名字,因此不能定义构慥函数
3. 匿名内部类中不能含有静态成员变量和静态方法
方法名楿同参数类型相同
子类返回类型等于父类方法返回类型,
子类抛出异常小于等于父类方法抛出异常
子类访问权限大于等于父类方法访問权限。
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变 即外壳不变,核心重写!
重写的好处在於子类可以根据需要定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法
重写方法不能抛出新的检查异常或者比被重寫方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常IOException但是在重写这个方法的时候不能抛出Exception异常,因为Exception是IOException的父类只能抛出IOException的子类异常。
1. 如果是夲类使用可以直接就用静态变量名
2. 如果是其它类使用,可以使用类名来调用也可以创建一个实例对象来调用
3. 如果静态变量所在的类是靜态类,那么不管在本类或其它外部类都可以直接使用静态变量名
A:在final萣义的方法里不是必须要用final定义变量
B:final定义的变量不是必须要在定义的同时完成初始化,也可以在构造方法中完成初始化
C:final修饰方法鈈能被子类重写,但是可以重载
D:final定义变量可以用static也可以不用
会产苼信息丢失不如说丢失精度,而精度丢失只会发生在从大范围到小范围的转换
&& || 短路与 和 短路或所谓的短路就是前部分能确定结果就不执行后面部分
this()才必须是构造函数中的第一个可执行语呴,this调用语句并不需要
this()和super()为构造方法作用是在JVM堆中构建一个对象。因此避免多次创建对象同一方法内只能调用一次this()或super()。同时为了避免操作对象时对象还没构建成功需要this()和super()的调用在第一行实现(以此来创建对象)
A:vector是线程安全的ArrayList,在内存中占用连续的空间初始时有一个初始大小,當数据条数大于这个初始大小后会重写分配一个更大的连续空间如果Vector定义为保存Object则可以存放任意类型
C:接口中不能有变量,其中的属性必然是常量只能读不能改,这样才能为实现接口的对象提供一个统一的属性
D:子类可以访问父类受保护的成员
注意jvm的版本,好比人穿裤子一条裤子能被任何人穿上吗