Java文件拷贝方式?哪一种最高效?

问题:

Java有几种文件拷贝方式?哪一种最高效?

知识点补充:

  1. 拷贝实现机制分析
  2. image

从上图可知,当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先在内核态讲数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。写入操作类似。

image

而基于NIO transferTo的实现方式,在Linux和Unix上,会使用零拷贝技术,数据传输并不需要用户态参与,省去了上下午切换的开销和不必要的内存拷贝,进而可提高应用拷贝性能。

注意:transferTo不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行Socket发送,同样可以享受这种机制带来的性能和扩展性提高。突然想到日志开发中使用到的mmap和其类似,又不一样,有兴趣可以参考:认真分析mmap:是什么 为什么 怎么用

回答问题:

Java有多种比较典型的文件拷贝实现方式,比如:
利用java.io类库,直接为源文件构建一个FileInputStream读取,然后再为目标文件构建一个FileOutputStream,完成写入工作。

1
2
3
4
5
6
7
8
9
10
public static void copyFileByStream(File source,File dest) throws IOException{
try(InputStream inputStream = new FileInputStream(source);
OutputStream outputStream = new FileOutputStream(dest)){
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer))>0){
outputStream.write(buffer,0,length);
}
}
}

或者,利用java.nio类库提供的transferTo或者transferFrom方法实现。

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
/**
* 复制文件,使用nio以提高性能
*
* @param src - 源文件
* @param dest - 目标文件
*/
public static void copyFile(File src, File dest) {
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel in = null;
FileChannel out = null;
try {
fis = new FileInputStream(src);
fos = new FileOutputStream(dest);
in = fis.getChannel(); // 得到对应的文件通道
out = fos.getChannel(); // 得到对应的文件通道
in.transferTo(0, in.size(), out); // 连接两个通道,并且从in通道读取,然后写入out通道
} catch (IOException e) {
e.printStackTrace();
} finally {
IoUtils.closeQuietly(fis);
IoUtils.closeQuietly(in);
IoUtils.closeQuietly(fos);
IoUtils.closeQuietly(out);
}
}

当然,Java标准类库本身已经提供了几种File.copy方法的实现(我在Android目前还没用到),或者使用使用Apache Commons IO提供的提供了一个FileUtils.copyFile(File from, File to)方法用于文件复制,如果项目里使用到了这个类库,使用这个方法是个不错的选择。它的内部也是使用Java NIO的FileChannel实现的(我在Android目前还没用到)。有兴趣可以参考:文件复制的 4 种实现方式及性能对比

对于Copy的效率,这个其实与操作系统和配置等情况有关。总体来说,NIO transferTo/From的方式可能更快,因为它更能利用现在操作系统底层机制,避免不必要的拷贝和上下文切换

参考:

声明:此为原创,转载请联系作者


作者:微信公众号添加公众号-遛狗的程序员 ,或者可以扫描以下二维码关注相关技术文章。

qrcode_for_gh_1ba0785324d6_430.jpg

当然喜爱技术,乐于分享的你也可以可以添加作者微信号:

WXCD.jpeg

FTP-作为Android开发,你应当了解

很久没更新文档了,1 : 确实很忙;2.自己学习的路还很长,知其然不知所以然的东西还很多。

知识点回顾

首先,我们来看下FTP相关定义:

  • 文件传输协议(英文:File Transfer Protocol,缩写:FTP)是用于在网络上进行文件传输的一套标准协议,使用客户/服务器模式。它属于网络传输协议的应用层。我们要分清文件传送(file transfer)和文件访问(file access)之间的区别,前者是FTP提供的,后者是如NFS等应用系统提供的。
  • FTP是一个8位的客户端-服务器协议,能操作任何类型的文件而不需要进一步处理,就像MIME或Unicode一样。但是,FTP有着极高的延时,这意味着,从开始请求到第一次接收需求数据之间的时间,会非常长;并且不时的必须执行一些冗长的登录进程。
  • FTP虽然可以被终端用户直接使用,但是它是设计成被FTP客户端程序所控制。
  • 运行FTP服务的许多站点都开放匿名服务,在这种设置下,用户不需要帐号就可以登录服务器,默认情况下,匿名用户的用户名是:“anonymous”。这个帐号不需要密码,虽然通常要求输入用户的邮件地址作为认证密码,但这只是一些细节或者此邮件地址根本不被确定,而是依赖于FTP服务器的配置情况。

Android中使用

在Android中我们可以使用第三方的库来操作FTP,比如Apache的包,commons-net-3.6.jar。下载地址http://commons.apache.org/proper/commons-net/download_net.cgi。

如何使用
  1. 初始化FTPClinet,代码如下:
1
ftpClient = new FTPClient();

2.设置登录地址和端口号,代码如下:

1
ftpClient.connect(TrackConstants.FTP_URL, TrackConstants.FTP_PORT);

3.设置登录用户名和密码,代码如下:

1
ftpClient.login(TrackConstants.FTP_USERNAME, TrackConstants.FTP_PWD);

4.设置文件类型和采用被动传输方式,代码如下:

1
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);

5.传输文件,代码如下:

1
2
3
//文件上传吧~
FileInputStream fileInputStream = new FileInputStream(FilePath);
ftpClient.storeFile(FileName, fileInputStream);

6.关闭连接,代码如下:

1
2
3
4
5
6
//关闭文件流
fileInputStream.close();
//退出登陆FTP,关闭ftpCLient的连接
ftpClient.logout();
ftpClient.disconnect();

小提醒

主动模式的FTP是指服务器主动连接客户端的数据端口,被动模式的FTP是指服务器被动地等待客户端连接自己的数据端口。

Android FTP客户端核心代码如下:
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
new Thread() {
@Override
public void run() {
super.run();
try {
//1.要连接的FTP服务器Url,Port
ftpClient.connect(TrackConstants.FTP_URL, TrackConstants.FTP_PORT);
//2.登陆FTP服务器
ftpClient.login(TrackConstants.FTP_USERNAME, TrackConstants.FTP_PWD);
//3.看返回的值是不是230,如果是,表示登陆成功
int reply = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
//断开
ftpClient.disconnect();
return;
}
//设置存储路径
ftpClient.makeDirectory("/data/" + directory );
ftpClient.changeWorkingDirectory("/data/" + directory);
//设置上传文件需要的一些基本信息
ftpClient.setBufferSize(1024);
ftpClient.setControlEncoding("UTF-8");
// ftpClient.enterLocalPassiveMode();
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
//文件上传吧~
FileInputStream fileInputStream = new FileInputStream(FilePath);
ftpClient.storeFile(FileName, fileInputStream);
//关闭文件流
fileInputStream.close();
//退出登陆FTP,关闭ftpCLient的连接
ftpClient.logout();
ftpClient.disconnect();
} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}.start();

作者:微信公众号添加公众号-遛狗的程序员 ,或者可以扫描以下二维码关注相关技术文章。

qrcode_for_gh_1ba0785324d6_430.jpg
当然喜爱技术,乐于分享的你也可以可以添加作者微信号:

WXCD.jpeg

HashMap、HashTable、TreeMap的理解

问题:

对比HashMap、HashTable、TreeMap有什么区别?

知识点

  1. Map相关的简单类图
    image
  2. hashcode、equals的一些约定
  • [x] equals相等,hashcode一定相等。
  • [x] 重写hashcode方法,也要重写equals方法。
  • [x] hashcode要保持一致性,状态改变后返回的哈希值仍然要一致。
  • [x] equals的对称、反射、传递等特性
  1. HashMap源码分析,后续文章会专门介绍(以及并发的时候无线循环占用CPU,size计算不准确等)
    部分核心精彩源码:
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
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

回答问题:

HashMap、HashTable、TreeMap都是常见的一些Map实现、是以键值对的形式存储和操作数据的容器类型。

HashTable是早期的Java类库提供的一个哈希表实现,本身是同步的,不支持null键和值,由于同步导致的性能开销,所以已经很少被推荐使用。

HashMap是应用更加广泛的哈希表的实现,行为上大致与HashTable一致,主要区别在于HashTable不是同步的,支持null值和键。通常情况下,HashMap进行put或者get操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选。

TreeMap是基于红黑树的一种提供顺序访问的Map,和HashMap不同,它的get、put、remove之类操作都是O(log(n))的时间复杂度,具体顺序可以由指定的Comparator来决定,或者根据键的自然顺序来判断。

参考:

  • 集合中部分源码
  • 极客时间APP核心技术第九讲| 对比Hashtable、HashMap、TreeMap有什么区别?
  • 自己写的Demo中部分代码

声明:此为原创,转载请联系作者


作者:微信公众号添加公众号-遛狗的程序员 ,或者可以扫描以下二维码关注相关技术文章。

qrcode_for_gh_1ba0785324d6_430.jpg

当然喜爱技术,乐于分享的你也可以可以添加作者微信号:

WXCD.jpeg

Android中Gradle深入理解

Gradle基础知识:

  • Groovy,由于它基于Java,所以我们仅介绍Java之外的东西。了解Groovy语言是掌握Gradle的基础。
  • Gradle作为一个工具,它的行话和它“为人处事”的原则。

Groovy定义:

百度百科:Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。由于其运行在 JVM 上的特性,Groovy 可以使用其他 Java 语言编写的库。

Groovy介绍:

image

发现了没?它和Java一样,也运行于Java虚拟机中!实际上,由于Groovy Code在真正执行的时候已经变成了Java字节码,所以JVM根本不知道自己运行的是Groovy代码。

Groovy入门:

请跳转:Groovy入门

这里重点讲一下闭包:

闭包,英文叫Closure,是Groovy中非常重要的一个数据类型或者说一种概念了。闭包的历史来源,种种好处我就不说了。我们直接看怎么使用它!

闭包,是一种数据类型,它代表了一段可执行的代码。其外形如下:

1
2
3
4
5
def aClosure = {//闭包是一段代码,所以需要用花括号括起来..
Stringparam1, int param2 -> //这个箭头很关键。箭头前面是参数定义,箭头后面是代码
println"this is code" //这是代码,最后一句是返回值,
//也可以使用return,和Groovy中普通函数一样
}

如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫it,和this的作用类似。it代表闭包的参数。

1
2
3
4
5
6
7
8
9
10
比如:
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
等同于:
def greeting = { it -> "Hello, $it!"}
assert greeting('Patrick') == 'Hello, Patrick!'
但是,如果在闭包定义时,采用下面这种写法,则表示闭包没有参数!
def noParamClosure = { -> true }
这个时候,我们就不能给noParamClosure传参数了!
noParamClosure ("test") <==报错喔!

闭包在Groovy中大量使用,比如很多类都定义了一些函数,这些函数最后一个参数都是一个闭包。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static <T> List<T>each(List<T> self, Closure closure)
上面这个函数表示针对List的每一个元素都会调用closure做一些处理。这里的closure,就有点回调函数的感觉。但是,在使用这个each函数的时候,我们传递一个怎样的Closure进去呢?比如:
def iamList = [1,2,3,4,5] //定义一个List
iamList.each{ //调用它的each,这段代码的格式看不懂了吧?each是个函数,圆括号去哪了?
println it
}
上面代码有两个知识点:
l each函数调用的圆括号不见了!原来,Groovy中,当函数的最后一个参数是闭包的话,可以省略圆括号。比如
def testClosure(int a1,String b1, Closure closure){
//dosomething
closure() //调用闭包
}
那么调用的时候,就可以免括号!
testClosure (4, "test", {
println"i am in closure"
} ) //红色的括号可以不写..

如经常使用到的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// apk output setting
applicationVariants.all { variant ->
variant.outputs.all { output ->
if (publish.equals('false') ) {
return
}
def releaseApkName = 'yhshop-' + variant.productFlavors[0].name + '-v' + versionName + '-' + versionCode + '.apk'
println("output name======" + releaseApkName)
outputFileName = releaseApkName
}
}

Android 应用模块的默认项目结构

[站外图片上传中…(image-5ebded-1533465873627)]

Gradle工作流程:

image

我们以上图应用模块的默认项目结构来介绍

  • 首先是初始化阶段。就比如执行如上图settings.gradle。
    Initiliazation phase的下一个阶段是Configration阶段。
  • Configration阶段的目标是解析每个project中的build.gradle。比如multi-project build例子中,解析每个子目录中的build.gradle。在这两个阶段之间,我们可以加一些定制化的Hook。这当然是通过API来添加的。
    Configuration阶段完了后,整个build的project以及内部的Task关系就确定了,一个Project包含很多Task,每个Task之间有依赖关系。Configuration会建立一个有向图来描述Task之间的依赖关系。所以,我们可以添加一个HOOK,即当Task关系图建立好后,执行一些操作。
  • 最后一个阶段就是执行任务了。当然,任务执行完后,我们还可以加Hook。

接下来就是具体API使用了:

Gradle 里的几乎任何东西都是基于这两个基础概念:

  • task
  • project
    掌握了这两个,你就掌握了一大半的 Gradle 知识了。
    具体可以参考:掌控 Android Gradle

这里我贴一下我自己Demo中的几个使用:

1
2
3
apply from: '../signature.gradle'
apply from: '../config.gradle'
apply plugin: 'kotlin-android'
1
2
3
4
5
6
7
8
9
if (project.hasProperty("product") ? "true" : "false") {
bugly {
//do something
}
} else {
bugly {
//do something
}
}
1
2
3
4
5
if (rootProject.ext.compileProject) {
//不同APP依赖不同模块
} else {
//不同APP依赖不同模块
}

参考:

声明:此为原创,转载请联系作者


作者:微信公众号添加公众号-遛狗的程序员 ,或者可以扫描以下二维码关注相关技术文章。

qrcode_for_gh_1ba0785324d6_430.jpg

当然喜爱技术,乐于分享的你也可以可以添加作者微信号:

WXCD.jpeg

HashMap源码1.7分析

前言:

HashMap 在 Java 和 Android 开发中非常常见。今天,我将带来HashMap 的全部源码分析,希望你们会喜欢。

本文基于版本 JDK 1.7,即 Java 7

1. 简介

1. 类定义

image

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* Hash table based implementation of the <tt>Map</tt> interface. This
* implementation provides all of the optional map operations, and permits
* <tt>null</tt> values and the <tt>null</tt> key. (The <tt>HashMap</tt>
* class is roughly equivalent to <tt>Hashtable</tt>, except that it is
* unsynchronized and permits nulls.) This class makes no guarantees as to
* the order of the map; in particular, it does not guarantee that the order
* will remain constant over time.
*
* <p>This implementation provides constant-time performance for the basic
* operations (<tt>get</tt> and <tt>put</tt>), assuming the hash function
* disperses the elements properly among the buckets. Iteration over
* collection views requires time proportional to the "capacity" of the
* <tt>HashMap</tt> instance (the number of buckets) plus its size (the number
* of key-value mappings). Thus, it's very important not to set the initial
* capacity too high (or the load factor too low) if iteration performance is
* important.
*
* <p>An instance of <tt>HashMap</tt> has two parameters that affect its
* performance: <i>initial capacity</i> and <i>load factor</i>. The
* <i>capacity</i> is the number of buckets in the hash table, and the initial
* capacity is simply the capacity at the time the hash table is created. The
* <i>load factor</i> is a measure of how full the hash table is allowed to
* get before its capacity is automatically increased. When the number of
* entries in the hash table exceeds the product of the load factor and the
* current capacity, the hash table is <i>rehashed</i> (that is, internal data
* structures are rebuilt) so that the hash table has approximately twice the
* number of buckets.
*
* <p>As a general rule, the default load factor (.75) offers a good
* tradeoff between time and space costs. Higher values decrease the
* space overhead but increase the lookup cost (reflected in most of
* the operations of the <tt>HashMap</tt> class, including
* <tt>get</tt> and <tt>put</tt>). The expected number of entries in
* the map and its load factor should be taken into account when
* setting its initial capacity, so as to minimize the number of
* rehash operations. If the initial capacity is greater than the
* maximum number of entries divided by the load factor, no rehash
* operations will ever occur.
*
* <p>If many mappings are to be stored in a <tt>HashMap</tt>
* instance, creating it with a sufficiently large capacity will allow
* the mappings to be stored more efficiently than letting it perform
* automatic rehashing as needed to grow the table. Note that using
* many keys with the same {@code hashCode()} is a sure way to slow
* down performance of any hash table. To ameliorate impact, when keys
* are {@link Comparable}, this class may use comparison order among
* keys to help break ties.
*
* <p><strong>Note that this implementation is not synchronized.</strong>
* If multiple threads access a hash map concurrently, and at least one of
* the threads modifies the map structurally, it <i>must</i> be
* synchronized externally. (A structural modification is any operation
* that adds or deletes one or more mappings; merely changing the value
* associated with a key that an instance already contains is not a
* structural modification.) This is typically accomplished by
* synchronizing on some object that naturally encapsulates the map.
*
* If no such object exists, the map should be "wrapped" using the
* {@link Collections#synchronizedMap Collections.synchronizedMap}
* method. This is best done at creation time, to prevent accidental
* unsynchronized access to the map:<pre>
* Map m = Collections.synchronizedMap(new HashMap(...));</pre>
*
* <p>The iterators returned by all of this class's "collection view methods"
* are <i>fail-fast</i>: if the map is structurally modified at any time after
* the iterator is created, in any way except through the iterator's own
* <tt>remove</tt> method, the iterator will throw a
* {@link ConcurrentModificationException}. Thus, in the face of concurrent
* modification, the iterator fails quickly and cleanly, rather than risking
* arbitrary, non-deterministic behavior at an undetermined time in the
* future.
*
* <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
* as it is, generally speaking, impossible to make any hard guarantees in the
* presence of unsynchronized concurrent modification. Fail-fast iterators
* throw <tt>ConcurrentModificationException</tt> on a best-effort basis.
* Therefore, it would be wrong to write a program that depended on this
* exception for its correctness: <i>the fail-fast behavior of iterators
* should be used only to detect bugs.</i>
*
* <p>This class is a member of the
* <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/collections/index.html">
* Java Collections Framework</a>.
*
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*
* @author Doug Lea
* @author Josh Bloch
* @author Arthur van Hoff
* @author Neal Gafter
* @see Object#hashCode()
* @see Collection
* @see Map
* @see TreeMap
* @see Hashtable
* @since 1.2
*/
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {

主要介绍

image

2.数据结构

2.1 具体描述

HashMap 采用的数据结构 = 数组(主) + 单链表(副),具体描述如下:

该数据结构方式也称:拉链法 有兴趣可以参考:开放定址法(线性探测),拉链法 -Hash算法

image

2.2 具体描述

image

2.3 存储流程

image

2.4 数组元素 & 链表节点的 实现类
  • HashMap中的数组元素 & 链表节点 采用 Entry类 实现,如下图所示:

image

  1. 即 HashMap的本质 = 1个存储Entry类对象的数组 + 多个单链表
  2. Entry对象本质 = 1个映射(键 - 值对),属性包括:键(key)、值(value) & 下1节点( next) = 单链表的指针 = 也是一个Entry对象,用于解决hash冲突
  • 该类的源码分析如下

    具体分析请看注释

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
81
82
83
/**
* Entry类实现了Map.Entry接口
* 即 实现了getKey()、getValue()、equals(Object o)和hashCode()等方法
**/
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; // 键
V value; // 值
Entry<K,V> next; // 指向下一个节点 ,也是一个Entry对象,从而形成解决hash冲突的单链表
int hash; // hash值
/**
* 构造方法,创建一个Entry
* 参数:哈希值h,键值k,值v、下一个节点n
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
// 返回 与 此项 对应的键
public final K getKey() {
return key;
}
// 返回 与 此项 对应的值
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
/**
* equals()
* 作用:判断2个Entry是否相等,必须key和value都相等,才返回true
*/
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
/**
* hashCode()
*/
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* 当向HashMap中添加元素时,即调用put(k,v)时,
* 对已经在HashMap中k位置进行v的覆盖时,会调用此方法
* 此处没做任何处理
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* 当从HashMap中删除了一个Entry时,会调用该函数
* 此处没做任何处理
*/
void recordRemoval(HashMap<K,V> m) {
}
}

具体使用

3.1 主要使用API(方法、函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
V get(Object key); // 获得指定键的值
V put(K key, V value); // 添加键值对
void putAll(Map<? extends K, ? extends V> m); // 将指定Map中的键值对 复制到 此Map中
V remove(Object key); // 删除该键值对
boolean containsKey(Object key); // 判断是否存在该键的键值对;是 则返回true
boolean containsValue(Object value); // 判断是否存在该值的键值对;是 则返回true
Set<K> keySet(); // 单独抽取key序列,将所有key生成一个Set
Collection<V> values(); // 单独value序列,将所有value生成一个Collection
void clear(); // 清除哈希表中的所有键值对
int size(); // 返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
boolean isEmpty(); // 判断HashMap是否为空;size == 0时 表示为 空
3.2 使用流程

在具体使用时,主要流程是:

- [x] 声明1个 HashMap的对象
- [x] 向 HashMap 添加数据(成对 放入 键 - 值对)
- [x] 获取 HashMap 的某个数据
- [x] 获取 HashMap 的全部数据:遍历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
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class HashMapTest {
public static void main(String[] args) {
/**
* 1. 声明1个 HashMap的对象
*/
Map<String, Integer> map = new HashMap<String, Integer>();
/**
* 2. 向HashMap添加数据(成对 放入 键 - 值对)
*/
map.put("Android", 1);
map.put("Java", 2);
map.put("iOS", 3);
map.put("数据挖掘", 4);
map.put("产品经理", 5);
/**
* 3. 获取 HashMap 的某个数据
*/
System.out.println("key = 产品经理时的值为:" + map.get("产品经理"));
/**
* 4. 获取 HashMap 的全部数据:遍历HashMap
* 核心思想:
* 步骤1:获得key-value对(Entry) 或 key 或 value的Set集合
* 步骤2:遍历上述Set集合(使用for循环 、 迭代器(Iterator)均可)
* 方法共有3种:分别针对 key-value对(Entry) 或 key 或 value
*/
// 方法1:获得key-value的Set集合 再遍历
System.out.println("方法1");
// 1. 获得key-value对(Entry)的Set集合
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
// 2. 遍历Set集合,从而获取key-value
// 2.1 通过for循环
for(Map.Entry<String, Integer> entry : entrySet){
System.out.print(entry.getKey());
System.out.println(entry.getValue());
}
System.out.println("----------");
// 2.2 通过迭代器:先获得key-value对(Entry)的Iterator,再循环遍历
Iterator iter1 = entrySet.iterator();
while (iter1.hasNext()) {
// 遍历时,需先获取entry,再分别获取key、value
Map.Entry entry = (Map.Entry) iter1.next();
System.out.print((String) entry.getKey());
System.out.println((Integer) entry.getValue());
}
// 方法2:获得key的Set集合 再遍历
System.out.println("方法2");
// 1. 获得key的Set集合
Set<String> keySet = map.keySet();
// 2. 遍历Set集合,从而获取key,再获取value
// 2.1 通过for循环
for(String key : keySet){
System.out.print(key);
System.out.println(map.get(key));
}
System.out.println("----------");
// 2.2 通过迭代器:先获得key的Iterator,再循环遍历
Iterator iter2 = keySet.iterator();
String key = null;
while (iter2.hasNext()) {
key = (String)iter2.next();
System.out.print(key);
System.out.println(map.get(key));
}
// 方法3:获得value的Set集合 再遍历
System.out.println("方法3");
// 1. 获得value的Set集合
Collection valueSet = map.values();
// 2. 遍历Set集合,从而获取value
// 2.1 获得values 的Iterator
Iterator iter3 = valueSet.iterator();
// 2.2 通过遍历,直接获取value
while (iter3.hasNext()) {
System.out.println(iter3.next());
}
}
}
// 注:对于遍历方式,推荐使用针对 key-value对(Entry)的方式:效率高
// 原因:
// 1. 对于 遍历keySet 、valueSet,实质上 = 遍历了2次:1 = 转为 iterator 迭代器遍历、2 = 从 HashMap 中取出 key 的 value 操作(通过 key 值 hashCode 和 equals 索引)
// 2. 对于 遍历 entrySet ,实质 = 遍历了1次 = 获取存储实体Entry(存储了key 和 value )

基础知识:HashMap中的重要参数(变量)

  • 在进行真正的源码分析前,先讲解HashMap中的重要参数(变量)
  • 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
26
27
// 1. 容量(capacity): HashMap中数组的长度
// a. 容量范围:必须是2的幂 & <最大容量(2的30次方)
// b. 初始容量 = 哈希表创建时的容量
// 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量 = 2的30次方(若传入的容量过大,将被最大值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 2. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
// a. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
// b. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)
// 实际加载因子
final float loadFactor;
// 默认加载因子 = 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 3. 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)
// a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
// b. 扩容阈值 = 容量 x 加载因子
int threshold;
// 4. 其他
// 存储数据的Entry类型 数组,长度 = 2的幂
// HashMap的实现方式 = 拉链法,Entry数组上的每个元素本质上是一个单向链表
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// HashMap的大小,即 HashMap中存储的键值对的数量
transient int size;

参考示意图:
image

此处 详细说明 加载因子

image

源码分析

  • 本次的源码分析主要是根据 使用步骤
  • 进行相关函数的详细分析
    主要分析内容如下

image

步骤1:声明1个 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
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
/**
* 函数使用原型
*/
Map<String,Integer> map = new HashMap<String,Integer>();
/**
* 源码分析:主要是HashMap的构造函数 = 4个
* 仅贴出关于HashMap构造函数的源码
*/
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable{
// 省略上节阐述的参数
/**
* 构造函数1:默认构造函数(无参)
* 加载因子 & 容量 = 默认 = 0.75、16
*/
public HashMap() {
// 实际上是调用构造函数3:指定“容量大小”和“加载因子”的构造函数
// 传入的指定容量 & 加载因子 = 默认
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* 构造函数2:指定“容量大小”的构造函数
* 加载因子 = 默认 = 0.75 、容量 = 指定大小
*/
public HashMap(int initialCapacity) {
// 实际上是调用指定“容量大小”和“加载因子”的构造函数
// 只是在传入的加载因子参数 = 默认加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 构造函数3:指定“容量大小”和“加载因子”的构造函数
* 加载因子 & 容量 = 自己指定
*/
public HashMap(int initialCapacity, float loadFactor) {
// HashMap的最大容量只能是MAXIMUM_CAPACITY,哪怕传入的 > 最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 设置 加载因子
this.loadFactor = loadFactor;
// 设置 扩容阈值 = 初始容量
// 注:此处不是真正的阈值,是为了扩展table,该阈值后面会重新计算,下面会详细讲解
threshold = initialCapacity;
init(); // 一个空方法用于未来的子对象扩展
}
/**
* 构造函数4:包含“子Map”的构造函数
* 即 构造出来的HashMap包含传入Map的映射关系
* 加载因子 & 容量 = 默认
*/
public HashMap(Map<? extends K, ? extends V> m) {
// 设置容量大小 & 加载因子 = 默认
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 该方法用于初始化 数组 & 阈值,下面会详细说明
inflateTable(threshold);
// 将传入的子Map中的全部元素逐个添加到HashMap中
putAllForCreate(m);
}
}

即 初始化数组(table)、扩容阈值(threshold)

注:

  • 此处仅用于接收初始容量大小(capacity)、加载因子(Load factor),但仍无真正初始化哈希表,即初始化存储数组table
  • 此处先给出结论:真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时。下面会详细说明

至此,关于HashMap的构造函数讲解完毕。

步骤2:向HashMap添加数据(成对 放入 键 - 值对)

添加数据的流程如下

注:为了让大家有个感性的认识,只是简单的画出存储流程,更加详细 & 具体的存储流程会在下面源码分析中给出

image

源码分析:

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
/**
* 函数使用原型
*/
map.put("Android", 1);
map.put("Java", 2);
map.put("iOS", 3);
map.put("数据挖掘", 4);
map.put("产品经理", 5);
/**
* 源码分析:主要分析: HashMap的put函数
*/
public V put(K key, V value)
(分析1)// 1. 若 哈希表未初始化(即 table为空)
// 则使用 构造函数时设置的阈值(即初始容量) 初始化 数组table
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 2. 判断key是否为空值null
(分析2)// 2.1 若key == null,则将该键-值 存放到数组table 中的第1个位置,即table [0]
// (本质:key = Null时,hash值 = 0,故存放到table[0]中)
// 该位置永远只有1个value,新传进来的value会覆盖旧的value
if (key == null)
return putForNullKey(value);
(分析3) // 2.2 若 key ≠ null,则计算存放数组 table 中的位置(下标、索引)
// a. 根据键值key计算hash值
int hash = hash(key);
// b. 根据hash值 最终获得 key对应存放的数组Table中位置
int i = indexFor(hash, table.length);
// 3. 判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表 逐个判断)
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
(分析4)// 3.1 若该key已存在(即 key-value已存在 ),则用 新value 替换 旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; //并返回旧的value
}
}
modCount++;
(分析5)// 3.2 若 该key不存在,则将“key-value”添加到table中
addEntry(hash, key, value, i);
return null;
}
  • 根据源码分析所作出的流程图

image

下面,我将根据上述流程的5个分析点进行详细讲解

分析1:初始化哈希表
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
/**
* 函数使用原型
*/
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
/**
* 源码分析:inflateTable(threshold);
*/
private void inflateTable(int toSize) {
// 1. 将传入的容量大小转化为:>传入容量大小的最小的2的次幂
// 即如果传入的是容量大小是19,那么转化后,初始化容量大小为32(即2的5次幂)
int capacity = roundUpToPowerOf2(toSize);->>分析1
// 2. 重新计算阈值 threshold = 容量 * 加载因子
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 3. 使用计算后的初始容量(已经是2的次幂) 初始化数组table(作为数组长度)
// 即 哈希表的容量大小 = 数组大小(长度)
table = new Entry[capacity]; //用该容量初始化table
initHashSeedAsNeeded(capacity);
}
/**
* 分析1:roundUpToPowerOf2(toSize)
* 作用:将传入的容量大小转化为:>传入容量大小的最小的2的幂
* 特别注意:容量大小必须为2的幂,该原因在下面的讲解会详细分析
*/
private static int roundUpToPowerOf2(int number) {
//若 容量超过了最大值,初始化容量设置为最大值 ;否则,设置为:>传入容量大小的最小的2的次幂
return number >= MAXIMUM_CAPACITY ?
MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
分析2:当 key ==null时,将该 key-value 的存储位置规定为数组table 中的第1个位置,即table [0]
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
/**
* 函数使用原型
*/
if (key == null)
return putForNullKey(value);
/**
* 源码分析:putForNullKey(value)
*/
private V putForNullKey(V value) {
// 遍历以table[0]为首的链表,寻找是否存在key==null 对应的键值对
// 1. 若有:则用新value 替换 旧value;同时返回旧的value值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 2 .若无key==null的键,那么调用addEntry(),将空键 & 对应的值封装到Entry中,并放到table[0]中
addEntry(0, null, value, 0);
// 注:
// a. addEntry()的第1个参数 = hash值 = 传入0
// b. 即 说明:当key = null时,也有hash值 = 0,所以HashMap的key 可为null
// c. 对比HashTable,由于HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
// d. 此处只需知道是将 key-value 添加到HashMap中即可,关于addEntry()的源码分析将等到下面再详细说明,
return null;
}

从此处可以看出:

  • HashMap的键key 可为null(区别于 HashTable的key 不可为null)
  • HashMap的键key 可为null且只能为1个,但值value可为null且为多个
分析3:计算存放数组 table 中的位置(即 数组下标 or 索引)
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
/**
* 函数使用原型
* 主要分为2步:计算hash值、根据hash值再计算得出最后数组位置
*/
// a. 根据键值key计算hash值 ->> 分析1
int hash = hash(key);
// b. 根据hash值 最终获得 key对应存放的数组Table中位置 ->> 分析2
int i = indexFor(hash, table.length);
/**
* 源码分析1:hash(key)
* 该函数在JDK 1.7 和 1.8 中的实现不同,但原理一样 = 扰动函数 = 使得根据key生成的哈希码(hash值)分布更加均匀、更具备随机性,避免出现hash值冲突(即指不同key但生成同1个hash值)
* JDK 1.7 做了9次扰动处理 = 4次位运算 + 5次异或运算
* JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
*/
// JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
static final int hash(int h) {
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
// 1. 取hashCode值: h = key.hashCode()
// 2. 高位参与低位的运算:h ^ (h >>> 16)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// a. 当key = null时,hash值 = 0,所以HashMap的key 可为null
// 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
// b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
}
/**
* 函数源码分析2:indexFor(hash, table.length)
* JDK 1.8中实际上无该函数,但原理相同,即具备类似作用的函数
*/
static int indexFor(int h, int length) {
return h & (length-1);
// 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
}

总结 计算存放在数组 table 中的位置(即数组下标、索引)的过程

image

在了解 如何计算存放数组table 中的位置 后,所谓 知其然 而 需知其所以然,下面我将讲解为什么要这样计算,即主要解答以下3个问题:

  1. 为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?
  2. 为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
  3. 为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?

在回答这3个问题前,请大家记住一个核心思想:

所有处理的根本目的,都是为了提高 存储key-value的数组下标位置 的随机性 & 分布均匀性,尽量避免出现hash值冲突。即:对于不同key,存储的数组下标位置要尽可能不一样

问题1:为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?

  • 结论:容易出现 哈希码 与 数组大小范围不匹配的情况,即 计算出来的哈希码可能 不在数组大小范围内,从而导致无法匹配存储位置
  • 原因描述:

image

  • 为了解决 “哈希码与数组大小范围不匹配” 的问题,HashMap给出了解决方案:哈希码 与运算(&) (数组长度-1);请继续问题2

问题2:为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?

  • 结论:容易出现 哈希码 与 数组大小范围不匹配的情况,即 计算出来的哈希码可能 不在数组大小范围内,从而导致无法匹配存储位置
  • 原因描述

image

问题3:为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?

  • 结论:加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突

  • 具体描述

image

至此,关于怎么计算 key-value 值存储在HashMap数组位置 & 为什么要这么计算,讲解完毕。

分析4:若对应的key已存在,则 使用 新value 替换 旧value

注:当发生 Hash冲突时,为了保证 键key的唯一性哈希表并不会马上在链表中插入新数据,而是先查找该 key是否已存在,若已存在,则替换即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 函数使用原型
*/
// 2. 判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表 逐个判断)
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 2.1 若该key已存在(即 key-value已存在 ),则用 新value 替换 旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; //并返回旧的value
}
}
modCount++;
// 2.2 若 该key不存在,则将“key-value”添加到table中
addEntry(hash, key, value, i);
return null;

分析5:若对应的key不存在,则将该“key-value”添加到数组table的对应位置中

函数源码分析如下

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
* 函数使用原型
*/
// 2. 判断该key对应的值是否已存在
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 2.1 若该key对应的值已存在,则用新的value取代旧的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 2.2 若 该key对应的值不存在,则将“key-value”添加到table中
addEntry(hash, key, value, i);
/**
* 源码分析:addEntry(hash, key, value, i)
* 作用:添加键值对(Entry )到 HashMap中
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
// 参数3 = 插入数组table的索引位置 = 数组下标
// 1. 插入前,先判断容量是否足够
// 1.1 若不足够,则进行扩容(2倍)、重新计算Hash值、重新计算存储数组下标
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); // a. 扩容2倍 --> 分析1
hash = (null != key) ? hash(key) : 0; // b. 重新计算该Key对应的hash值
bucketIndex = indexFor(hash, table.length); // c. 重新计算该Key对应的hash值的存储数组下标位置
}
// 1.2 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中--> 分析2
createEntry(hash, key, value, bucketIndex);
}
/**
* 分析1:resize(2 * table.length)
* 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
*/
void resize(int newCapacity) {
// 1. 保存旧数组(old table)
Entry[] oldTable = table;
// 2. 保存旧容量(old capacity ),即数组长度
int oldCapacity = oldTable.length;
// 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 4. 根据新容量(2倍容量)新建1个数组,即新table
Entry[] newTable = new Entry[newCapacity];
// 5. 将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1
transfer(newTable);
// 6. 新数组table引用到HashMap的table属性上
table = newTable;
// 7. 重新设置阈值
threshold = (int)(newCapacity * loadFactor);
}
/**
* 分析1.1:transfer(newTable);
* 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容
* 过程:按旧链表的正序遍历链表、在新链表的头部依次插入
*/
void transfer(Entry[] newTable) {
// 1. src引用了旧数组
Entry[] src = table;
// 2. 获取新数组的大小 = 获取新容量大小
int newCapacity = newTable.length;
// 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
for (int j = 0; j < src.length; j++) {
// 3.1 取得旧数组的每个元素
Entry<K,V> e = src[j];
if (e != null) {
// 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)
src[j] = null;
do {
// 3.3 遍历 以该数组元素为首 的链表
// 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
Entry<K,V> next = e.next;
// 3.4 重新计算每个元素的存储位置
int i = indexFor(e.hash, newCapacity);
// 3.5 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中
// 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入
e.next = newTable[i];
newTable[i] = e;
// 3.6 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
e = next;
} while (e != null);
// 如此不断循环,直到遍历完数组上的所有数据元素
}
}
}
/**
* 分析2:createEntry(hash, key, value, bucketIndex);
* 作用: 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
// 1. 把table中该位置原来的Entry保存
Entry<K,V> e = table[bucketIndex];
// 2. 在table中该位置新建一个Entry:将原头结点位置(数组上)的键值对 放入到(链表)后1个节点中、将需插入的键值对 放入到头结点中(数组上)-> 从而形成链表
// 即 在插入元素时,是在链表头插入的,table中的每个位置永远只保存最新插入的Entry,旧的Entry则放入到链表中(即 解决Hash冲突)
table[bucketIndex] = new Entry<>(hash, key, value, e);
// 3. 哈希表的键值对数量计数增加
size++;
}

此处有2点需特别注意:键值对的添加方式 & 扩容机制

参考:

声明:此为原创,转载请联系作者


作者:微信公众号添加公众号-遛狗的程序员 ,或者可以扫描以下二维码关注相关技术文章。

qrcode_for_gh_1ba0785324d6_430.jpg

当然喜爱技术,乐于分享的你也可以可以添加作者微信号:

WXCD.jpeg

Vector、ArrayList、LinkedList的理解

最近感悟:天将降大任于斯人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为,所以动心忍性,曾益其所不能。

问题:

对比Vector、ArrayList、LinkedList有何区别?

知识点补充

  1. 集合框架的整体设计(并发包这里没有添加进来)
    image
  • [x] List:也就是我们介绍的有序集合,它提供了方便的访问、插入、删除操作。
  • [x] Set:Set是不允许重复元素的,也就是不存在两个对象equals返回true。我们日常开发中很多需要保证元素唯一性的场合。
  • [x] Queue/Deque:Java提供的标准队列实现,支持FIFO、LIL等特定行为。
  1. Set基本特征和典型使用场景。
  • [x] TreeSet:TreeSet代码里实现实际默认是利用TreeMap实现的,所有插入的元素以键的形式插入到TreeMap里面,支持自然排序访问,但是插入、删除、包含等操作相对低效(Log(n)时间)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Constructs a new, empty tree set, sorted according to the
* natural ordering of its elements. All elements inserted into
* the set must implement the {@link Comparable} interface.
* Furthermore, all such elements must be <i>mutually
* comparable</i>: {@code e1.compareTo(e2)} must not throw a
* {@code ClassCastException} for any elements {@code e1} and
* {@code e2} in the set. If the user attempts to add an element
* to the set that violates this constraint (for example, the user
* attempts to add a string element to a set whose elements are
* integers), the {@code add} call will throw a
* {@code ClassCastException}.
*/
public TreeSet() {
this(new TreeMap<E,Object>());
}

这里有一个典型的例子,在开发相关支付请求参数进行签名的时候会对请求参数进行进行自然排序,如支付宝支付:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Map<String, String> getSortedMap(RequestParametersHolder requestHolder) {
Map<String, String> sortedParams = new TreeMap<String, String>();
AlipayHashMap appParams = requestHolder.getApplicationParams();
if (appParams != null && appParams.size() > 0) {
sortedParams.putAll(appParams);
}
AlipayHashMap protocalMustParams = requestHolder.getProtocalMustParams();
if (protocalMustParams != null && protocalMustParams.size() > 0) {
sortedParams.putAll(protocalMustParams);
}
AlipayHashMap protocalOptParams = requestHolder.getProtocalOptParams();
if (protocalOptParams != null && protocalOptParams.size() > 0) {
sortedParams.putAll(protocalOptParams);
}
return sortedParams;
}

  1. HashSet通过Hash算法,理想情况下,如果哈希散列正常,可以提供常数时间的添加、删除、包含等操作,但是它不保证有序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ReactMethod
public void setPushTime(ReadableMap map) {
try {
mContext = getCurrentActivity();
ReadableArray array = map.getArray("days");
Set<Integer> days = new HashSet<Integer>();
for (int i=0; i < array.size(); i++) {
days.add(array.getInt(i));
}
int startHour = map.getInt("startHour");
int endHour = map.getInt("endHour");
JPushInterface.setPushTime(mContext, days, startHour, endHour);
} catch (Exception e) {
e.printStackTrace();
}
}
  1. LinkedHashSet:内部构建了一个记录插入顺序的双向链表,因此提供了按照插入顺序遍历的能力,与此同时,也提供了常数时间的添加、插入、删除、包含操作。性能略低于HashSet,因为需要维护链表的开销。
1
2
3
4
5
6
/**
* Constructs a new empty instance of {@code LinkedHashSet}.
*/
public LinkedHashSet() {
super(new LinkedHashMap<E, HashSet<E>>());
}

从源码中可以看出,HashSet其实以HashMap为基础实现的。

  1. 在遍历元素的时候,HashSet性能受自身容量影响,所以初始化的时候,除非有必要,不然不要将其背后的HashMap容量设置过大。而对于LinkedHashMap遍历性能只和元素个数有关系。
  2. 上面所说的集合都是线程不安全的,除了并发包中线程安全容器,在Collections也提供了一些线程安全工具类(简单粗暴的synchronized保证)来保证线程安全。如:

    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
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    static class SynchronizedCollection<E> implements Collection<E>, Serializable {
    private static final long serialVersionUID = 3053995032091335093L;
    final Collection<E> c;
    final Object mutex;
    SynchronizedCollection(Collection<E> var1) {
    this.c = (Collection)Objects.requireNonNull(var1);
    this.mutex = this;
    }
    SynchronizedCollection(Collection<E> var1, Object var2) {
    this.c = (Collection)Objects.requireNonNull(var1);
    this.mutex = Objects.requireNonNull(var2);
    }
    public int size() {
    Object var1 = this.mutex;
    synchronized(this.mutex) {
    return this.c.size();
    }
    }
    public boolean isEmpty() {
    Object var1 = this.mutex;
    synchronized(this.mutex) {
    return this.c.isEmpty();
    }
    }
    public boolean contains(Object var1) {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    return this.c.contains(var1);
    }
    }
    public Object[] toArray() {
    Object var1 = this.mutex;
    synchronized(this.mutex) {
    return this.c.toArray();
    }
    }
    public <T> T[] toArray(T[] var1) {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    return this.c.toArray(var1);
    }
    }
    public Iterator<E> iterator() {
    return this.c.iterator();
    }
    public boolean add(E var1) {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    return this.c.add(var1);
    }
    }
    public boolean remove(Object var1) {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    return this.c.remove(var1);
    }
    }
    public boolean containsAll(Collection<?> var1) {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    return this.c.containsAll(var1);
    }
    }
    public boolean addAll(Collection<? extends E> var1) {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    return this.c.addAll(var1);
    }
    }
    public boolean removeAll(Collection<?> var1) {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    return this.c.removeAll(var1);
    }
    }
    public boolean retainAll(Collection<?> var1) {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    return this.c.retainAll(var1);
    }
    }
    public void clear() {
    Object var1 = this.mutex;
    synchronized(this.mutex) {
    this.c.clear();
    }
    }
    public String toString() {
    Object var1 = this.mutex;
    synchronized(this.mutex) {
    return this.c.toString();
    }
    }
    public void forEach(Consumer<? super E> var1) {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    this.c.forEach(var1);
    }
    }
    public boolean removeIf(Predicate<? super E> var1) {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    return this.c.removeIf(var1);
    }
    }
    public Spliterator<E> spliterator() {
    return this.c.spliterator();
    }
    public Stream<E> stream() {
    return this.c.stream();
    }
    public Stream<E> parallelStream() {
    return this.c.parallelStream();
    }
    private void writeObject(ObjectOutputStream var1) throws IOException {
    Object var2 = this.mutex;
    synchronized(this.mutex) {
    var1.defaultWriteObject();
    }
    }
    }
  3. 集合中提供的默认排序算法,具体是什么排序方式和设计思路。(这个在后续讲解算法的时候会详细介绍)

  4. Java9中标准类库中提供了一系列静态工厂方法。比如List.of()、Set.of(),大大简化了构建小的容器实例的代码量。
1
List<String> simpleList = List.of("hello","world");

回答问题

这三者都是实现集合框架中的List,也就是所谓的有序集合,具体功能上基本类似,比如:都能按照位置进行定位、都能插入和删除,都提供迭代器以遍历内容。但是在具体行为、性能、线程安全又有很大的差别。
Vector是Java早期提供的线程安全的动态数组,如果不需要线程安全,并不建议使用,毕竟同步是消耗性能的。Vector使用动态数组来保存数据,可以根据需要自动增加容量,当数组已经满的时候,会创建新的数组,并拷贝原有的数组数据。
ArrayList是应用更加广泛的集合,内部使用动态数组实现,线程不安全,所以效率会比较高。同样,根据需要自动增加容量,但是和Vector不一样,扩容的时候增加50%,而Vector是增加一倍。
LinkedList内部使用双向链表实现,便于插入和删除,不利于查询,也是线程不安全的。

参考:

  • 集合中部分源码
  • 极客时间APP核心技术第七讲| List simpleList = List.of(“hello”,”world”);
  • 自己写的Demo中部分代码

声明:此为原创,转载请联系作者


作者:微信公众号添加公众号-遛狗的程序员 ,或者可以扫描以下二维码关注相关技术文章。

qrcode_for_gh_1ba0785324d6_430.jpg

当然喜爱技术,乐于分享的你也可以可以添加作者微信号:

WXCD.jpeg

为何Twitter区别于微信、淘宝,只使用了armeabi-v7a?

最近在研究APP瘦身,碰巧又遇到armeabi、armeabi-v7a、arm64-v8a等ABI相关的知识点,决心记录下来以作分享。

目前现状

首先我们分析下国内的淘宝、微信,以及国外的Facebook、Twitter都使用了哪些ABI。

我们对这4家APK进行Analyze,可以发现Facebook和Twitter只使用了armeabi-v7a,而微信和淘宝只使用了armeabi,分析结果如下图所示:
WechatIMG17.jpeg大厂并没有按照我们的理解使用不同的ABI针对不同的CPU?其实笔者发现携程、饿了么、百度糯米都是只使用了armeabi,阿里系的淘票票使用了armeabi、x86(如果你有兴趣,可以通过爬取分析一下应用市场的前100名都使用了哪些ABI)。

知识点回顾

首先,我们来看下Google老大哥是怎么介绍的ABI的,翻译官方文档:

不同 Android 手机使用不同的 CPU,因此支持不同的指令集

CPU 与指令集的每种组合都有其自己的应用二进制界面(或 ABI)

ABI 可以非常精确地定义应用的机器代码在运行时如何与系统交互

您必须为应用要使用的每个 CPU 架构指定 ABI

表 1. ABI 和支持的指令集。

WechatIMG18.jpeg
各版本分析如下:

  • mips / mips64: 极少用于手机可以忽
  • x86 / x86_64: x86 架构的手机都会包含由 Intel 提供的称为 Houdini 的指令集动态转码工具,实现对 arm .so 的兼容,再考虑 x86 1% 以下的市场占有率,x86 相关的两个 .so 也是可以忽略的
  • armeabi: ARM v5 这是相当老旧的一个版本,缺少对浮点数计算的硬件支持,在需要大量计算时有性能瓶颈
  • armeabi-v7a: ARM v7 目前主流版本
  • arm64-v8a: 64位支持

具体含义

如果你基础比较好的话,看完Google老大哥的介绍的话,就明白了:不同的 CPU 支持不同的指令集。当我们需要我们APP支持尽可能多的不同CPU的时候,只需要将不同版本的so文件放置在不同的目录下,APK安装运行的时候会根据自己需要而自己选取。

但是这样却带来一个问题,APK文件较大,影响用户下载。

那我们是否可以只放置一些呢?

必然可以!

我们继续看Google老大哥是怎么说的:

  • 为实现最佳性能,应直接针对主要 ABI进行编译。例如,基于 ARMv5TE的典型设备只会定义主要 ABI:armeabi
  • 相反,基于ARMv7的典型设备将主要ABI定义为armeabi-v7a,而将辅助ABI定义为armeabi,因为它可以运行为每个ABI生成的应用原生二进制文件
  • 许多基于x86的设备也可行 armeabi-v7a 和 armeabi NDK 二进制文件
  • 对于这些设备,主要ABI将是 x86,辅助ABI是armeabi-v7a
  • 基于 MIPS的典型设备只定义主要ABI:mips
  • 安装应用时,软件包管理器服务将扫描APK,查找以下形式的任何共享库:

    1
    2
    > lib/<primary-abi>/lib<name>.so
    >
  • 如果未找到,并且您已定义辅助 ABI,该服务将扫描以下形式的共享库

    1
    2
    > lib/<secondary-abi>/lib<name>.so
    >
  • 找到所需的库时,软件包管理器会将它们复制到应用的 data 目录 (data/data//lib/) 下的/lib/lib.so

分析

上面说,如果根本没有共享对象文件,应用也会构建并安装,但在运行时会崩溃。所以有时候我们遇到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Exception:Java.lang.UnsatisfiedLinkError: dlopen failed: library “/***.so” not found
```
此时我们首先要想到的,是不是so文件没有放置,或者是在armeabi放置a.so,b.so,但是在armeabi-v7a只放置了b.so,没有放置a.so。
这里强调一下:虽然arm64-v8a是可以向下兼容的,但是也是兼容的有限制的:
其下有armeabi-v7a,armeabi ;armeabi-v7a向下兼容armeabi。对于一个cpu是arm64-v8a架构的手机,它运行app时,进入jnilibs去读取库文件时,先看有没有arm64-v8a文件夹,如果没有该文件夹,去找armeabi-v7a文件夹,如果没有,再去找armeabi文件夹,如果连这个文件夹也没有,就抛出异常; 如果有arm64-v8a文件夹,那么就去找特定名称的.so文件。
如果有arm64-v8a文件夹,那么就去找特定名称的.so文件,注意:如果没有找到,不会再往下(armeabi-v7a文件夹)找了,而是直接抛出异常。
#### 注意:
1. 如果没有找到,不会再往下(armeabi-v7a文件夹)找了,而是直接抛出异常。
2. 如果你的项目用到了第三方依赖,如果只保留一个ABI的时候,建议在Build中加入ndk.abiFilters
3.
例如:第三方aar文件,如果这个sdk对abi的支持比较全,可能会包含armeabi、armeabi-v7a、x86、arm64-v8a、x86_64五种abi,而你应用的其它so只支持armeabi、armeabi-v7a、x86三种,直接引用sdk的aar,会自动编译出支持5种abi的包。但是应用的其它so缺少对其它两种abi的支持,那么如果应用运行于arm64-v8a、x86_64为首选abi的设备上时,就会==crash==了哦。
```
defaultConfig {
ndk {
abiFilters "armeabi"// 指定ndk需要兼容的ABI(这样其他依赖包里x86,armeabi,arm-v8之类的so会被过滤掉)
}
}

总结

  1. 如果你希望APK针对不同CPU有不同的版本,你可以使用胖二进制的方式,在不同的目录下面放置不同的so文件。这样兼容性更广、性能好些,但是APK大些;

  2. 如果你希望APK小一些,你可以像淘宝、微信、FaceBook、Twitter一样,一个api闯天下,至于选择armeabi、armeabi-v7a看你市场用户了。目前主流手机cpu多属于armeabi-v7a;

  3. 当然,你也可以动态检查系统环境,如果是x86就去下载相关库,然后加载……这样可以减少apk体积。


作者:微信公众号添加公众号-遛狗的程序员 ,或者可以扫描以下二维码关注相关技术文章。

qrcode_for_gh_1ba0785324d6_430.jpg
当然喜爱技术,乐于分享的你也可以可以添加作者微信号:

WXCD.jpeg

微信小程序 (应用号) 微信 web 开发者工具破解版

MAC 官方版:https://yunpan.cn/ckvBk8WXK742d (提取码:421d)
windows官方版:https://yunpan.cn/ckvBEzAbn9CPX (提取码:7ca5)

破解文件,直接替换就行,Windows,Mac:https://yunpan.cn/ckvBRK4hydtsg (提取码:7c8b)

如果大家特别懒,直接下载我已经替换好的。

Mac破解版:https://yunpan.cn/ckvBbavJRwt25 (提取码:a869)
Windows破解版:https://yunpan.cn/ckvMx4hncQSvN (提取码:111a)

demo下载:https://yunpan.cn/ckvvQVGgak6tx (提取码:7141)

开发文档:http://notedown.cn/weixin/api/

原文链接:破解版本:https://github.com/gavinkwoe/weapp-ide-crack

原文链接:http://www.diycode.cc/topics/308

事件分发机制之-Button的onTouch()事件分析

##基础知识:
1.本篇文章就不分析晦涩难懂的源码了(加上源码后文章内容太长,建议有耐心的读者自己查看源码来验证文章的分析结果)。在这里通过简单的代码直接打印日志让你清晰的认识Button的OnTouch事件传递机制(和onClick,onLongClick事件发生先后顺序)。
2.在onTouch事件中:down事件返回值标记此次事件是否为点击事件(返回false,是点击事件;返回true,不记为点击事件),而up事件标记此次事件结束时间,也就是判断是否为长按。

####一 . onTouch返回false

1.首先是onTouch事件的down事件发生,此时,如果长按,触发onLongClick事件;
2.然后是onTouch事件的up事件发生,up完毕,最后触发onClick事件。

日志:
不长按.png

长按.png

####二 . onTouch返回true

1.首先是onTouch事件的down事件发生,然后是onTouch事件的up事件发生;
2.期间不触发onClick和onLongClick事件

日志:
不长按.png

长按.png

####三 . onTouch:down返回true,up返回false:结果同二
1.首先是onTouch事件的down事件发生,然后是onTouch事件的up事件发生;
2.期间不触发onClick和onLongClick事件

日志:

不长按.png

长按.png

机制分析:

  1. onTouch事件中:down事件返回值标记此次事件是否为点击事件(返回false,是点击事件;返回true,不记为点击事件),

  2. 而up事件标记此次事件结束时间,也就是判断是否为长按。

3.只要当down返回true时候,系统将不把本次事件记录为点击事件,也就不会触发onClick或者onLongClick事件了。

4.因此尽管当up的时候返回false,系统也不会继续触发onClick事件了。

####四 . onTouch:down返回false,up返回true:
1.首先是onTouch事件的down事件发生,此时:
2.长按,触发onLongClick事件,然后是onTouch事件的up事件发生,完毕。
3.短按,先触发onTouch的up事件, 到一定时间后,自动触发onLongClick事件。

日志:

不长按.png

长按.png

机制分析:

  1. onTouch事件中:down事件返回值标记此次事件是否为点击事件(返回false,是点击事件;返回true,不记为点击事件),

  2. 而up事件标记此次事件结束时间,也就是判断是否为长按。

3.只要当down返回true时候,系统将把本次事件记录为点击事件,而up返回了true,表示一直没有结束,一直长按中,也就不会触发onClick事件了。

源代码:

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package com.example.h.myapplication;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private final String TAG = MainActivity.class.getSimpleName();
private Button bntTest1,bntTest2,bntTest3,bntTest4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bntTest1 = (Button) findViewById(R.id.bnt_test1);
bntTest2 = (Button) findViewById(R.id.bnt_test2);
bntTest3 = (Button) findViewById(R.id.bnt_test3);
bntTest4 = (Button) findViewById(R.id.bnt_test4);
bntTest1.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.e(TAG,"--bntTest1--onOnLongClick()触发----");
return false;
}
});
bntTest1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG,"--bntTest1---onClick()触发----");
}
});
bntTest1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// Log.e(TAG,"--bntTest1---MotionEvent触发----"+event.getAction());
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--bntTest1---MotionEvent.ACTION_DOWN:触发----");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--bntTest1---MotionEvent.ACTION_UP:触发----");
break;
}
return false;
}
});
bntTest2.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.e(TAG,"--bntTest2--onOnLongClick()触发----");
return false;
}
});
bntTest2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG,"--bntTest2---onClick()触发----");
}
});
bntTest2.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--bntTest2---MotionEvent.ACTION_DOWN:触发----");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--bntTest2---MotionEvent.ACTION_UP:触发----");
break;
}
return true;
}
});
bntTest3.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.e(TAG,"--bntTest3--onOnLongClick()触发----");
return false;
}
});
bntTest3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG,"--bntTest3---onClick()触发----");
}
});
bntTest3.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--bntTest3---MotionEvent.ACTION_DOWN:触发----");
return true;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--bntTest3---MotionEvent.ACTION_UP:触发----");
return false;
}
return false;
}
});
bntTest4.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.e(TAG,"--bntTest4--onOnLongClick()触发----");
return false;
}
});
bntTest4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG,"--bntTest4---onClick()触发----");
}
});
bntTest4.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--bntTest4---MotionEvent.ACTION_DOWN:触发----");
return false;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--bntTest4---MotionEvent.ACTION_UP:触发----");
return true;
}
return false;
}
});
}
}

##结语
本篇文章分析了Android事件分发机制之Button的OnTouch事件,为了方便读者理解,只需要记住down事件返回值标记此次事件是否为点击事件(返回false,是点击事件;返回true,不记为点击事件),而up事件标记此次事件结束时间这个关键点就好了。

##项目地址
https://github.com/ruanjiankeji/ButtonEventAnalysis

如有不足,欢迎小伙伴指正,相互学习;如果觉得还可以,欢迎文章star或者github上star、follow

|