《Java 核心技术卷Ⅰ》
《Java 核心技术卷Ⅰ》
int 和 Integer
int a = 100, b =100;
System.out.println(a == b); //true
Integer c = 100, d =100;
System.out.println(c == d); //true
int e = 1000, f =1000;
System.out.println(e == f); //true
Integer g = 1000, h =1000;
System.out.println(g == h); //false
首先,int 是基本数据来类型之一,而 Integer 是 int 的包装类。
一个小知识点,“ = =” 它是用来比较是否相等的:
- 如果“ = =”两边是基本数据类型,例如 int a = 100;那 int 类型的 100 自然是等于 100 的,1000 和 1000 自然也是相等的。
- 如果“ = =”两边是对象类型,则会比较两个对象的地址,不同的对象一般情况下地址当然是不同的,g 和 h 就是两个不同的 Integer 对象,地址自然不同,也就不成立。
那为什么 c 和 d 会相等呢?这涉及到 java 中一个自动装箱的概念,将基本数据类型转为包装类型叫做装箱,反过来叫拆箱。在对包装类进行一些算数运算时,会执行自动装箱和拆箱,而这个自动的范围是有限制的,对于 int 来讲,范围是 -128~127 之间,于是 Integer 对象的值为 100 的时候可以执行自动拆箱,结果自然是 100 等于 100,而值为 1000 时,超出了自动拆箱的范围,不再比较值而是比较地址,于是结果是 false。
异常
异常分为非检查型异常和检查型异常。
非检查型:派生于 Error 类或 RuntimeException 类的所有异常;
检查型:除上述所有其他异常。
编译器将检查你是否为所有检查型异常提供了异常处理器。
所有的异常都是由 Throwable 继承而来,下一层立即分解为两个分支:Error 和 Exception,而 Exception 又分为 RuntimeException 和 IOException 两个分支。
链表
在我们程序日常应用中,有很多地方都用到了数组以及动态的数组列表,但是他们有一个很大的缺陷,就是从数组中间删除删除一个出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动(见图 1)。 在数组中间的位置上插入一个元素也是如此。
而另外一个数据结构——链表就可以解决这个问题。数组是在连续的存储位置上存放对象引用,而链表则是将每个对象存放在单独的链接中。在 Java 中,所有的链表都是双向链接。
从链表中删除一个元素很简单,只需要将这个元素相邻的两个元素链接起来,将要删除的元素从链接中剔除即可(见图 2),插入也是相同的道理。
而链表不支持类似数组下标访问的方式,来访问其中的第 n 个元素,必须从头开始,越过 n-1 个元素,这就让人挺难过的。因此在需要按整数索引(下标)来访问元素时,通常不会选择链表,使用它的唯一理由是尽可能地减少在列表中间插入或删除元素的开销。
——摘自《Java 核心技术卷 Ⅰ》
常见集合,映射
如果哪天处理数据你发现你正在使用的方法对于要解决的问题效率不高,可能是因为你使用了错误的数据结构。
- 数组/数组列表: 可以使用索引很方便的访问到指定位置的数据,但有一个重大的缺陷就是在数组中间添加或删除一个元素开销很大。
- 链表: 而对于链表而言能在很大程度减小这个开销,但是它失去了快速随机访问,哪怕使用 get(index)方法,依旧是进行遍历访问。
- 散列集: 链表和数组允许我们指定元素的次序,但是当我们想查看某个元素却又不记得它的位置就需要访问所有元素直到找到为止,如果不在意元素的顺序,有几种可以能够快速查询元素的数据结构,缺点是无法控制元素出现的次序,例如散列表。
- 树集: TreeSet 类相比散列集有所改进,它是一个有序集合。可以以任意顺序插入元素,在对集合遍历时,值将自动地按照排序后的顺序呈现。而且,与检查数组或链表中的重复元素相比,使用树会快很多。当然,将一个元素添加到树中要比添加到散列表中慢,而且要使用树集,必须能够比较元素。
- 映射: 上面所说的,集是一个集合,允许快速查找现有的元素,但是通常情况下我们会知道某些关键信息,希望去查找与之关联的元素。映射便是为此设计,它用来存放键值对,也是我们很熟悉的结构。
数组
在我们程序日常应用中,有很多地方都用到了数组以及动态的 ArrayList,但是他们有一个很大的缺陷,就是从数组中间删除一个元素需要付出很大的代价,原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动, 在数组中间的位置上插入一个元素也是一样的道理。
链表
而另外一个数据结构——链表就可以解决这个问题。数组是在连续的存储位置上存放对象引用,而链表则是将每个对象存放在单独的链接中。在 Java 中,所有的链表都是双向链接,每个链接中还存放着指向前驱的引用。从链表中删除一个元素很简单,只需要将这个元素相邻的两个元素链接起来,将要删除的元素从链接中剔除即可。例如链表中分别有 A、B、C 三个链接,如果要删除 B,只需要将 A 指向 B 的引用替换成 A 指向 C 的引用;将 C 指向 B 的引用替换成 C 指向 A 的引用。插入也是相同的道理。
链表是一个有序集合,每个对象的位置十分重要。LinkedList 类中的 add 方法可以将对象添加到链表的尾部。但是我们之所以使用链表的一大原因就是需要将元素添加到链表的中间,而这种依赖于位置的 add 方法将有迭代器负责,下面是链表具体使用的两个简单小例子。
链表的简单使用 1:先添加 3 个元素,再删除第二个元素
public static void main(String[] args) {
LinkedList<String> students = new LinkedList<String>();
students.add("Xiao Ming");
students.add("Xiao Hong");
students.add("Xiao Hua");
Iterator<String> iter = students.iterator();
String first = iter.next();
System.out.println("first:" + first); //first:Xiao Ming
String second = iter.next();
System.out.println("second:" + second); //second:Xiao Hong
iter.remove();
System.out.println("Now students have:" + students); //Now students have:[Xiao Ming, Xiao Hua]
}
链表的简单使用 2:先添加 3 个元素,在第一个与第二个元素之中添加一个元素
public static void main(String[] args) {
LinkedList<String> students = new LinkedList<String>();
students.add("Xiao Ming");
students.add("Xiao Hong");
students.add("Xiao Hua");
ListIterator<String> iter = students.listIterator();
iter.next();
iter.add("New Student");
System.out.println("Now students have:" + students); //Now students have:[Xiao Ming, New Student, Xiao Hong, Xiao Hua]
}
要注意以上两个代码快中的迭代器是不一样的哦!
链表的一个缺点是不支持快速随机访问,它不像数组那样,可以通过索引直接访问。要查看链表中的第 n 个元素,就必须从头开始,越过 n-1 个元素。虽然 LinkedList 类还是提供了一个用来访问某个特定元素的 get 方法,但是这个方法的本质 6 依旧是从头开始遍历。尽管现在的 get(n)方法做了一个微小的优化——如果 n > size()/2 ,就从列表尾端开始搜索元素,但 get 方法依旧效率不太高。很明显我们使用链表的唯一理由是尽可能地减少在列表中间插入或删除元素的开销。
散列表
链表和数组可以按照我们的意愿排列元素,当我们想要查看某个指定的元素,却忘记了它的位置,就只能通过遍历所有元素直到找到该元素为止。可想而知,当集合中包含很多元素,这样查找将会耗费很多时间。如果我们不需要在意集合中元素的顺序,有几种能够快速查找元素的数据结构,例如散列表。散列表会为每一个对象计算一个整数,这个整数称为散列码(hash code),是的,hash code,正如你所想的那样,这个散列码和 hashCode 方法联系紧密。String 类的对象的散列码就是有 String 类中的 hashCode 方法产生的。而如果我们需要自定义类,就要负责实现这个类的 hashCode 方法,这里要注意,自己实现的 hashCode 方法要与 equals 方法兼容!这些不是本文重点不再赘述。
在 Java 中,散列表用链表数组实现。每个列表被称为桶(bucket),要想查找表中对象的位置,就要先计算出它的散列码,然后与桶的总数取余,余数就是保存这个元素的桶的索引。在元素插入桶中时,有时候会遇到桶已经被填充的情况。这种现象被称为散列冲突。这时需要先将新对象与桶中已经存在的对象进行比较,查看这个对象是否已经存在。如果散列码合理地随机分布,桶的数目也足够大,需要比较的次数会比较少。
散列表是根据关键码值(Key-value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
看起来十分复杂,但其实就是通过键值对的方式存储和查找。而这种操作,与我们经常会用到的 HashMap
非常相似,事实上,HashMap
内部就是散列表数据结构,因此我们完全可以通过常用的 HashMap 来帮助我们理解散列表。可以看 1.8 中文文档中关于 HashMap 的介绍:
Hash table based implementation of the
Map
interface. This implementation provides all of the optional map operations, and permitsnull
values and thenull
key. (TheHashMap
class is roughly equivalent toHashtable
, 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.This implementation provides constant-time performance for the basic operations (
get
andput
), assuming the hash function disperses the elements properly among the buckets. Iteration over collection views requires time proportional to the "capacity" of theHashMap
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.
HashMap 基于哈希表的实现的 Map
接口。该实现提供了所有可选的映射操作,并允许 null 值和 null 键。( HashMap 类大致相当于
Hashtable ,除了它是异步的,并允许 null)。这个类不能保证映射的顺序,特别是,它不能保证顺序在一段时间内保持不变。 原因是,如果元素的散列码发生了改变,元素在数据结构中的位置也会发生变化,因此在更改散列表中的元素是要格外小心!
树集
TreeSet 相对散列集有所改进,它是一个有序集合,可以以任意顺序将元素插入到集合中,在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现(排序使用树结构完成的,当前实现使用的是红黑树)。每次将一个元素添加到树中时,都将放在正确的排序位置上,因此将一个元素添加到树中要比添加到散列表中慢,但是与添加到数组或链表的正确位置上相比还是快很多的。
树集的简单使用:
public static void main(String[] args) {
TreeSet<Student> students = new TreeSet<Student>();
students.add(new Student("Xiao Ming",19));
students.add(new Student("Xiao Hong",20));
students.add(new Student("Xiao Hua",18));
System.out.println(students);
}
public class Student {
private String name;
private Integer age;
public Student (String name,Integer age){
this.name = name;
this.age = age;
}
}
执行结果:会抛出一个 异常:java.lang.ClassCastException
Exception in thread "main" java.lang.ClassCastException: com.macro.mall.demo.dto.Student cannot be cast to java.lang.Comparable
at java.util.TreeMap.compare(TreeMap.java:1294)
at java.util.TreeMap.put(TreeMap.java:538)
at java.util.TreeSet.add(TreeSet.java:255)
at com.macro.mall.demo.controller.Test.main(Test.java:40)
原因在于我们需要告诉 TreeSet 如何来进行比较元素,如果不指定,就会抛出这个异常。首先,我们需要在自定义类(Student)中实现 Comparable
接口,并重写接口中的 compareTo 方法,这里我使用了最简单最简单的例子,应该没有更简单的例子了:
public class Student implements Comparable<Student> {
private String name;
private Integer age;
public Student (String name,Integer age){
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student o) {
int result = Integer.compare(age,o.age);
return result;
}
}
输出结果:[Student(name=Xiao Hua, age=18), Student(name=Xiao Ming, age=19), Student(name=Xiao Hong, age=20)]
映射
集是一个集合,它可以快速地查找现有的元素,但是在集合中查找元素通常要进行遍历查询。而我们有时候知道某些键的信息,并且想通过键去查找到对应的元素,映射表就是为了解决这个问题而诞生的。映射存放的是我们非常熟悉的键值对,值可以重复,键是唯一的。
Java 类库提供了两个通用的实现,也是我们最常用到的两个类:HashMap 和 TreeMap,这两个类都实现了 Map 接口。散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树。与集一样,散列映射比树映射要快,因此如果不需要按照有序的顺序访问键,最好选择散列映射。
有趣的是,集合框架本身不认为映射是一个集合,而其他数据结构框架认为映射是一个键值对集合,或者说是按键索引的值集合。通常,我们会通过得到映射的视图来得到集合,有三种视图:键集、值集合、键值对集。对应的方法为:
Set<K> KeySet()
Collection<V> values()
Set<Map.Entry<K, V>> entrySet()
下面举一个小例子来看一下映射的使用,依旧是简单至上:
public static void main(String[] args) {
HashMap<Integer,Student> students = new HashMap<Integer,Student>();
students.put(1,new Student("Xiao Ming",18));
students.put(2,new Student("Xiao Hong",18));
students.put(3,new Student("Xiao Hua",18));
students.forEach((k,v) ->
System.out.println( "id:" + k +" name:"+ v.getName() + " age:" + v.getAge() ) );
}
// 输出结果:
// id:1 name:Xiao Ming age:18
// id:2 name:Xiao Hong age:18
// id:3 name:Xiao Hua age:18
关于 Java 中的几个常见数据结构就简单介绍到这,重点是对比不同的数据结构的优缺点,在合适的环境使用合适的数据结构。大家可以自行去深入研究每一个数据结构,Fighting!
多线程
多进程与多线程有着本质的区别,每个进程都拥有自己的一套变量,而线程则共享数据。线程最简单的 3 个状态分别是:New(新建),Runnable(可执行),Blocked(阻塞)。当用 new 创建一个线程时,这个线程还没有开始运行,它的状态便是 New;一旦调用 start 方法,线程就会处于 Runnable 状态,只不过它可能在运行也可能并没有运行,它不一定始终保持运行,可能会小睡一会儿以让其他线程运行。这依赖于线程调度器,它会决定哪个线程运行哪个线程被踢出,而我们不能很好地保证。线程调度器也可能因为某些原因让某些线程暂时处于不可运行的状态,即为阻塞。
当多线程需要共享同一对象而且都有可以修改对象状态的方法时,常会引发竞态条件(race condition),会引发数据的损毁。例如一个典型的问题——丢失更新。举一个简单的例子,线程 A 和线程 B 都可以对某一个整型数据进行读写。A 有三个步骤分别是读取,将读出来的值 +1,写入;B 有三个步骤分别是读取,将读出来的值 +2,写入;当线程 A 执行时,读取的值为 0,计算应该写入的值为 1,这时,线程调度器将 A 踢出了,B 开始执行。由于 A 还没有写入,因此 B 读取的值也为 0,计算应该写入的值为 2,于是这个数据的值变为了 2,这时候,线程调度器又随机安排 A 执行,而 A 并不知道刚刚 B 做了什么,它就像睡着了失去了意识,睡醒之后继续工作,将 1 写入数据,于是这个数据的值变为了 1,此时线程 B 刚刚对数据的更新完全消失了,这便是“丢失更新”。
这种问题可以使用同步机制来解决,给对象上锁,确保使任何时刻都只有一个线程进入对象,并且在该线程执行结束前不会将钥匙交给其他线程。