Long Luo's Life Notes

每一天都是奇迹

HashMap也是我们使用非常多的Collection,它是基于哈希表的Map接口的实现,以key-value的形式存在。在HashMap中,key-value总是会当做一个整体来处理,系统会根据hash算法来来计算key-value的存储位置,我们总是可以通过key快速地存、取value。下面就来分析HashMap的存取。

一、定义

HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供Map接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实AbstractMap类已经实现了Map,这里标注Map LZ觉得应该是更加清晰吧!

1
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable

二、构造函数

HashMap提供了三个构造函数:

  • HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空HashMap。
  • HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空HashMap。
  • HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空HashMap。

在这里提到了两个参数:初始容量,加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。

HashMap是一种支持快速存取的数据结构,要了解它的性能必须要了解它的数据结构。

三、数据结构

我们知道在Java中最常用的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以利用这两种来组合实现,HashMap也是如此。实际上HashMap是一个“链表散列”,如下是它数据结构:

HashMap数据结构图:

从上图我们可以看出HashMap底层实现还是数组,只是数组的每一项都是一条链。其中参数initialCapacity就代表了该数组的长度。下面为HashMap构造函数的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能<0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: "
+ initialCapacity);
//初始容量不能 > 最大容量值,HashMap的最大容量值为2^30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子不能 < 0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: "
+ loadFactor);

// 计算出大于 initialCapacity 的最小的 2 的 n 次方值。
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;

this.loadFactor = loadFactor;
//设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作
threshold = (int) (capacity * loadFactor);
//初始化table数组
table = new Entry[capacity];
init();
}

从源码中可以看出,每次新建一个HashMap时,都会初始化一个table数组。table数组的元素为Entry节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;

/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
.......
}

其中Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,这是非常重要的,正是由于Entry才构成了table数组的项为链表。

上面简单分析了HashMap的数据结构,下面将探讨HashMap是如何实现快速存取的。

阅读全文 »

不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题。Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎。大多数待遇丰厚的Java开发职位都要求开发者精通多线程技术并且有丰富的Java程序开发、调试、优化经验,所以线程相关的问题在面试中经常会被提到。

在典型的Java面试中,面试官会从线程的基本概念问起,如:为什么你需要使用线程,如何创建线程,用什么方式创建线程比较好(比如:继承thread类还是调用Runnable接口,然后逐渐问到并发问题像在Java并发编程的过程中遇到了什么挑战,Java内存模型,JDK1.5引入了哪些更高阶的并发工具,并发编程常用的设计模式,经典多线程问题如生产者消费者,哲学家就餐,读写器或者简单的有界缓冲区问题。仅仅知道线程的基本概念是远远不够的,你必须知道如何处理死锁,竞态条件,内存冲突和线程安全等并发问题。掌握了这些技巧,你就可以轻松应对多线程和并发面试了。

1. 什么是线程?

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成该任务只需10毫秒。Java在语言层面对多线程提供了卓越的支持,它也是一个很好的卖点。

2. 线程和进程有什么区别?

线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

3. 如何在Java中实现线程?

在语言层面有两种方式。java.lang.Thread类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,由于线程类本身就是调用的Runnable接口所以你可以继承java.lang.Thread类或者直接调用Runnable接口来重写run()方法实现线程。

4. 用Runnable还是Thread?

这个问题是上题的后续,大家都知道我们可以通过继承Thread类或者调用Runnable接口来实现线程,问题是,哪个方法更好呢?什么情况下使用它?这个问题很容易回答,如果你知道Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口好了。

6. Thread类中的start()和run()方法有什么区别?

这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。 start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程

7. Java中Runnable和Callable有什么不同?

Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。它们的主要区别是Callable的call()方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。

8. Java中CyclicBarrier和CountDownLatch有什么不同?

CyclicBarrier和CountDownLatch都可以用来让一组线程等待其它线程。与CyclicBarrier不同的是,CountdownLatch不能重新使用。

9. Java内存模型是什么?

Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行。这个关系定义了一些规则让程序员在并发编程时思路更清晰。

  • 线程内的代码能够按先后顺序执行,这被称为程序次序规则。
  • 对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
  • 前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
  • 一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。
  • 一个线程的所有操作都会在线程终止之前,线程终止规则。
  • 一个对象的终结操作必须在这个对象构造完成之后,也叫对象终结规则。
  • 可传递性

我强烈建议大家阅读《Java并发编程实践》第十六章来加深对Java内存模型的理解。

10. Java中的volatile变量是什么?

volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生,就是上一题的volatile变量规则。

11. 什么是线程安全?Vector是一个线程安全类吗?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector是用同步方法来实现线程安全的,而和它相似的ArrayList不是线程安全的。

12. Java中什么是竞态条件?举个例子说明。

竞态条件会导致程序在并发情况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的bugs。这种bugs很难发现而且会重复出现,因为线程间的随机竞争。一个例子就是无序处理,详见答案。

13. Java中如何停止一个线程?

Java提供了很丰富的API但没有为停止线程提供API。JDK1.0本来有一些像stop(),suspend()和resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后JavaAPI的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。 当run()或者call()方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile布尔变量来退出run()方法的循环或者是取消任务来中断线程。

14. 一个线程运行时发生异常会怎样?

这是我在一次面试中遇到的一个很刁钻的Java面试题,简单的说,如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。 当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。

15. 如何在两个线程间共享数据?

你可以通过共享对象来实现这个目的,或者是使用像阻塞队列这样并发的数据结构。这篇教程《Java线程间通信》(涉及到在两个线程间共享对象)用wait和notify方法实现了生产者消费者模型。

16. Java中notify和notifyAll有什么区别?

这又是一个刁钻的问题,因为多线程可以等待单监控锁,Java API的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。

17. 为什么wait,notify和notifyAll这些方法不在thread类里面?

这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

18. 什么是ThreadLocal变量?

ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。 首先,通过复用减少了代价高昂的对象的创建个数;其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。

阅读全文 »

By Long Luo

  1. 一个.java源文件中是否可以包括多个类(不是内部类)?有什么限制?

可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致

  1. Java有没有goto?

Java中的保留字,现在没有在java中使用。

  1. 说说&&&的区别。

&&&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。

&&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式,例如,对于if(str != null && !str.equals("")表达式,当str为null时,后面的表达式不会执行,所以不会出现NullPointerException如果将&&改为&,则会抛出NullPointerException异常。if(x==33 & ++y>0) y会增长,if(x==33 && ++y>0)不会增长

&还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如,0x31 & 0x0f的结果为0x01。

  1. switch语句能否作用在byte上,能否作用在long上,能否作用在String上?

switch(expr1)中,expr1只能是一个整数表达式或者枚举常量,整数表达式可以是int基本类型或Integer包装类型,由于,byte, short, char都可以隐含转换为int,所以,这些类型以及这些类型的包装类型也是可以的。显然,long和String类型都不符合switch的语法规定,并且不能被隐式转换成int类型,所以,它们不能作用于swtich语句中。

  1. short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 += 1;有什么错?

对于short s1 = 1; s1 = s1 + 1;由于s1+1运算时会自动提升表达式的类型,所以结果是int型,再赋值给short类型s1时,编译器将报告需要强制转换类型的错误。

对于short s1 = 1; s1 += 1;由于 += 是java语言规定的运算符,java编译器会对它进行特殊处理,因此可以正确编译。

  1. char型变量中能不能存贮一个中文汉字?为什么?

char型变量是用来存储Unicode编码的字符的,unicode编码字符集中包含了汉字,所以,char型变量中当然可以存储汉字啦。不过,如果某个特殊的汉字没有被包含在unicode编码字符集中,那么,这个char型变量中就不能存储这个特殊汉字。补充说明:unicode编码占用两个字节,所以,char类型的变量也是占用两个字节。

  1. 使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?

使用final关键字修饰一个变量时,是指引用变量不能变,引用变量所指向的对象中的内容还是可以改变的。

例如,对于如下语句:

1
final StringBuffer a = new StringBuffer("immutable");

执行如下语句将报告编译期错误:

1
a=new StringBuffer("");

但是,执行如下语句则可以通过编译:

1
a.append(" broken!"); 

有人在定义方法的参数时,可能想采用如下形式来阻止方法内部修改传进来的参数对象:

1
2
3
public void method(final  StringBuffer  param) {

}

实际上,这是办不到的,在该方法内部仍然可以增加如下代码来修改参数对象:

1
param.append("a");
阅读全文 »

By Long Luo

通过爬虫抓取的历年收藏的一些微博和网络上语录,时常翻看,会对社会对世界对人生有更清晰的认识…


财富即人性。

所有的大财富,归根结底就是,你对人性的透彻理解。人们都需要的东西,就是你提前打桩的位置。穿越周期,靠的是你对人性的理解。你也不要急了,很多事情,我现在和你讲,你现在理解不了,例如什么叫与人为善。等你到四十岁就明白了。屌丝怒了, ~等我到40岁来不及了,大长腿都老了! ~没关系,上半场先扒拉个小短腿。还有下半场,老红也是女人嘛,你凑合吃两口。 越过道德的边境,我们走过爱的禁区。


落地。

以前有个屌丝和我说了心里话。他也愿意脚踏实地,拼命工作,早日发财。但他希望,有个师傅,把卤鸡爪的秘诀告诉他,再给他找个不错的门面房,装修好,锅碗瓢盆买好,再把进货渠道和质量把关的方法告诉他,他就可以安安心心做事业了。你现在让我从零开始,一点一点摸索,我完全没有头绪啊!你安的是什么心?这实在太累了,我不想发财了,算了,算了。我还是去空调房打游戏,摇一摇,最好捞个不要钱的二手女人,我不嫌弃,不花钱就好。

我完全理解这个屌丝。当然,我还最好当个甩手掌柜,你把公司给我,我让老婆去操心,我干收钱!不香吗?事实上,人世间没有这个好事。为什么没有这好事?因为你爹二十年前也是这么想的。吃光用光,身体健康!儿孙自有儿孙福!辣块妈妈滴!


很多人希望别人上赶着来伺候自己。

老子愿意学贴砖,你告诉我,哪里学? 老子愿意发财,你告诉我哪里发? 老子愿意当上门女婿,你告诉我,给几套房? 册那,我又不是你老子,我干嘛操心你啊?你以为你那“苞米脑壳红苕心”有人感兴趣啊?西开! 人生所有的飞黄腾达,都是自己琢磨出来的,真正的发财路子,没人会跑来教你。2009年,某生意火爆,我超级感兴趣,找了好多人问,没人告诉我。那么如何找到适合自己的路子? 一是多听多看。你不是牛逼吗?你就多听多看,不懂就研究,你倒是说个一二三啊!我家门口有个小超市,十年不倒,我就奇了怪了,凭什么?我不停的研究,最后发现原因了。这个超市是自有房产,大部分货是赊账来的,根本没有成本。我后来才知道,世界上居然还有赊账生意。 二是研究聪明人。聪明人有一些小窍门,传男不传女,密不透风。但是,绝大多数人总有打盹的时候,你多接触多研究,总可以破门而入。菜嫂自以为挺聪明的,被我一把拿下,现在我已经是MBA全球总裁班的人了。 三是顺势而为。你千万不要觉得,世人皆醉我独醒。这个世界,跟风生意最好做,当然,前提是你不要想赚最后一个铜板,赚一把就走。很多人不肯走,最后就废了。 多年前,菜嫂家某个生意特别赚钱,手底下的人都跑出去自立门户了,拦也拦不住。菜嫂一看挣钱多,就想扩大产能。被我活活摁住,最后换了套房子,册那,女方家说好给我买个房的,不能耍赖。于是,我就过上了奶油蛋糕的幸福生活。 四是认清自己。很多人认不清自己。总觉得自己快三十岁了,混的一笔吊糟,是因为没有伯乐。要是哪个土豪拉我一把,给我个十亿单子,我也发达了。实际上,没有怀才不遇,而是你压根没有才。 你不是牛吗?你的房呢?你的车呢?你的那几个小红呢?呵呵都没有吧?这就是现实。不要七想八想了。 条条大路通罗马。发财的路子千万条,你要有打游戏的劲头,尽可能找到一条。不要太贪心,只要一条就发了。 小结,去萧山的“躺赢专列”已经很便宜了,你还想怎么样?


不要急于否定别人。

这个世界有太多太多我们不知道的事,你偶尔听到了,那是你的幸运。但是,有太多的屌丝,一听到与自己苞谷脑壳认知不一样的事,第一反应就是急匆匆的否定。这种人已经彻底封闭自己了,年纪二十岁,脑壳七十岁。 我年轻时候,有个朋友和我说,中环靠近外环的孙桥别墅三千一平米,非常漂亮,要不要去看看?我当时大怒,你和我说这个干嘛?我要的是外滩贵州路。我急急忙忙的拒绝了人生最大一个彩蛋。后来这个别墅因为误工逾期,开发商还赔了一笔钱,算下来大概是2700元一平米。 这种事情非常多。我以前觉得自己什么不懂!要你说?对于任何人的建议都嗤之以鼻,就你个二本子,也想说我不好?滚!然后我就成功的把自己封闭起来了,二十年后,妥妥的一根老油条,还骂天骂地! 老子是大学生啊!连个房子也没有,上下五千年都对不起我。册那,我年轻时候也够屌的。


结婚意味着什么?

大部分屌丝不知道答案。他们以为,就是丈母娘把女儿洗的干干净净给你送过来,你舒舒服服用。实际上远远不是这么简单。婚后,你可能没几年就有娃了。这就要你老命了。 1,老婆和闺蜜比房子。你吃得消吧?也不多,千万入门,脱层皮。 2,娃娃是个吞金兽。一年七八万,还要24小时配合班级微信群的家长任务,又要脱层皮。 3,自己也要考职称。副高不是那么容易的。脱层皮。 4,如果你还不服,没关系,再过几年,就可以去三甲收费处刷卡了,上不封顶,四个老人,你一人准备80w,省着点花,大差不差了。 当然,你也可以选择躺死狗,那你倒大霉了。老婆闹,儿子哭,老头老太冲上门,破口大骂,骂你不孝!你自己整两杯牛二吧!

阅读全文 »

By Long Luo

题目:

将一个n元一维数组a[n]左移i个位置。例如,当n=8,i=3时,数组abcdefgh旋转为defghabc。请设计一个算法完成这个任务。

解答

杂技算法

分析:将a[0]存储在一个临时变量中,然后将a[i]替换a[0],a[2i]替换a[i]….当一个循环结束的时候,若替换次数小于n,则从a[1]开始替换…,需要经过gcd(n,i)(n和i的最大公约数)次循环后,才能把每一个元素都移到该移的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/*
**
** tcpipstack @29/10/2012.
** shenzhen.
**
*/
#include <iostream>
#include <string>

using namespace std;

/* Acrobat Rotate Shift Alogrithm */
string AcrobatRotateShift(string str, int n);

//
int main(void)
{
string str1 = "abcdefg";
string str2 = "";

cout<<"str1= "<<str1<<endl;
cout<<"str2= "<<str2<<endl;


str2 = AcrobatRotateShift(str1, 2);

cout<<"str1= "<<str1<<endl;
cout<<"str2= "<<str2<<endl;

getchar();

return 0;
}


/*
*
* the Acrobat Rotate Shift Alogrithm
*
*/
string AcrobatRotateShift(string str, int n)
{
int strlen = str.length();
int count = 0; //统计移动次数
int i, j = 0; //k初始化为0,从str[0]开始
char temp;

while (1)
{
//开始的元素保存到临时变量temp中
temp = str[j];
i = (j + n) % strlen;

//开始移动,直到遇到开始的元素
while (i != j)
{
str[(i - n + strlen) % strlen] = str[i];
count++; //移动次数统计量+1
i = (i + n) % strlen;
}

//临时变量temp中保存的值赋值给刚才移动的最后一个位置
str[(j - n + strlen) % strlen] = temp;
count++;

//判断是否所有元素都已经移动
if (count < strlen)
{
//没有移动所有元素,再次从str[j+1]开始
j++;
}
else
{
//所有元素都已经移动,跳出循环
break;
}
}

return str;
}
阅读全文 »
0%