Java容器学习笔记

  • 容器(Collection)
    • 1.泛型Generics
      • 1.2 自定义泛型
      • 1.3 容器中使用泛型
    • 2.Collection接口
      • 2.1 List特点和常用方法
      • 2.2 ArrayList的底层实现
      • 2.3 LinkedList特点和底层实现
      • 2.4 Vector向量
    • 3.Map接口
      • 3.1 HashMap底层实现
        • 3.1.1 HashMap基本结构讲解 ( 这里以JDK1.6中的HashMap为例 )
        • 3.2.1 存储数据过程put(key, value>)
        • 3.2.2 取数据过程get(key)
        • 3.2.3 扩容问题
        • 3.2.4 JDK8 将链表在大于 8 情况下变为黑红二叉树
        • 3.2.5 通过编写自定义的HashMap来理解HashMap底层实现
      • 3.2 TreeMap的使用和底层实现
      • 3.3 HashMap和HashTable
    • 4.Set接口
      • 4.1 HashSet基本使用
      • 4.2 HashSet的底层实现
      • 4.3 TreeSet的使用和底层实现
    • 5.迭代器介绍
      • 5.1 遍历集合的方法总结
    • 6.Collections工具类
    • 7.使用容器来存储表格

容器(Collection)

1.泛型Generics

​ 容器,是用来装其他对象的一个对象。

​ 比如数组,本身就是一种容器,可以在其中放置对象或基本数据类型。

​ 数据的优势:是简单的现行序列,可以快速地访问数组元素,效率高。如果从效率和类型检查的角度讲,数组是最好的。

​ 数组的劣势:不灵活。容器需要事先定义好,不能随着需求的变化而扩容。比如:我们在一个用户管理系统中,要把今天注册的所有用户取出来,那么这样的用户有多少个?我们在写程序时是无法确定的。因此,在这里就不能使用数组。

​ 接下来是容器的类图结构:

!


其中,Set接口没有顺序,不能重复;而List接口有顺序可以重复。Map是用来存放键值对的。

​ 泛型是JDK1.5以后增加的,它可以帮助我们建立类型安全的集合。在使用了泛型的集合中,遍历时不必进行强制类型转换。JDK提供了支持泛型的编译器,将运行时的类型检查提前到编译时执行,提高了代码可读性和安全性。

​ 泛型的本质就是"数据类型的参数化",我们可以把"泛型"理解为数据类型的一个占位符(形式参数),即告诉编译器,在调用泛型时,必须传入实际类型。

一个简单的容器的例子:

package collection;/*** 测试容器*/public class TestGeneric {public static void main(String[] args) {MyCollection mc = new MyCollection();mc.set("String", 0);mc.set(1253, 1);Object obj = mc.get(0);System.out.println(obj.toString());}}class MyCollection {Object[] objects = new Object[5];public void set(Object obj, int index) {if(index < 5 && index >= 0) {objects[index] = obj;}elsrongqie{System.out.println("Rang is wrong!");}}public Object get(int index) {if(index < 5 && index >= 0) {return objects[index];}else{return null;}}}

1.2 自定义泛型

​ 我们可以在类的声明处增加泛型列表,如 <T, E, V>。

​ 此处,字符可以是任何标志符,一般采用这3个字母。

​ 下面是一个简单的泛型的使用例子:

package collection;/*** 测试泛型*/public class TestGeneric {public static void main(String[] args) {MyCollection<String> mc = new MyCollection<>();mc.set("String", 0);mc.set("1253", 1);String obj = mc.get(0);System.out.println(obj); // 默认调用了 .toString()方法}}class MyCollection<E> {Object[] objects = new Object[5];public void set(E e, int index) {if(index < 5 && index >= 0) {objects[index] = e;}else{System.out.println("Rang is wrong!");}}public E get(int index) {if(index < 5 && index >= 0) {return (E)objects[index];}else{return null;}}
leixing
}
/**
* 这里的E相当于提前传入了一个定义的类型。
*/

1.3 容器中使用泛型

​ 容器相关类都定义了泛型,我们在开发和工作中,使用容器类时都要使用泛型。这样,在容器的存储数据、读取数据时都避免了大量的判断类型,非常便捷。

示例:泛型类的在集合中的使用

public class Test {public static void main(String[] args) {//以下代码中List、Set、Map、Iterator都是与容器相关的接口List<String> list = new ArraryList<>();Set<Man> mans = new HashSet<>();Map<Integer, Man> maps = new HashMap<Integer, Man>();Iterator<Man> iterator = mans.iterator();}
}

2.Collection接口

​ Collection表示一组对象,它是集中、收集的意思。Collection接口的两个子接口是List、Set接口。

​ 下表是 Collection接口中定义的方法:

方法 说明
boolean add(Object element) 增加元素到容器中
boolean remove(Object element) 从容器中移除元素
boolean contains(Object element) 容器中是否包含该元素
boolean isEmpty() 容器是否为空
int size() 容器中元素的数量
void clear() 清空容器中所有元素
Iterator iterator() 获得迭代器,用于遍历所有元素
boolean containsAll(Collection c) 本容器是否包含c容器中的所有元素
boolean addAll(Collection c) 将容器c中的所有元素增加到本容器中
boolean removeAll(Collection c) 移除本容器和容器c中都包含的元素
boolean retainAll(Collection c) 取本容器和容器c中都包含的元素,移除非交换元素
Object[] toArray() 转化成Object数组

​ 由于List、Set是Collection的子接口,意味着所有的List、Set的实现类都有上面的方法。

​ 下面是一些例子:

package collection;import java.util.ArrayList;
import java.util.Collection;/*** 测试List方法*/public class TestList {public static void main(String[] args) {Collection<String> c = new ArrayList<>();// 增加元素c.add("钱前");c.add("倩倩");System.out.println(c);System.out.println(c.size());// 元素的大小System.out.println(c.isEmpty());// 元素是否为空System.out.println("===========");// 转化出一个Object[]数组Object[] objs = c.toArray();System.out.println(objs);System.out.println("==========");// 返回是否包含该元素System.out.println(c.contains("倩倩"));System.out.println(c.contains("高老五"));System.out.println("==========");c.remove("倩倩"); // 删除指定对象System.out.println(c);System.out.println("==========");c.clear(); // 删除所有的对象System.out.println(c);System.out.println(c.size());}}/*
* [钱前, 倩倩]
* 2
* false
* ===========
* [Ljava.lang.Object;@5e2de80c
* ==========
* true
* false
* ==========
* [钱前]
* ==========
* []
* 0
*/

​ 我们在使用Collection这个容器时,增加或者删除容器中的对象时,只是将对象的地址值存入或者从容器中删除,而实际上对象依然存在。

​ 接下来是剩下来的一些方法,这些方法是涉及到两个容器之间的操作。

public static void restFunction() {List<String> list01 = new ArrayList<>();list01.add("aa");list01.add("bb");list01.add("cc");List<String> list02 = new ArrayList<>();list02.add("aa");list02.add("dd");list02.add("ee");System.out.println("list01: " + list01);System.out.println("===========");// 传入对象的所有的元素加入原数组list01.addAll(list02);// 将两个容器中相同的对象删除list01.removeAll(list02);// 输出两个容器中相同对象list01.retainAll(list02);System.out.println("list01: " + list01);System.out.println("===========");System.out.println(list01.containsAll(list02));}

2.1 List特点和常用方法

​ List是有序、可重复的容器。

​ 有序:List中每一个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素,从而精确控制这些元素。

​ 可重复:List允许加入重复的元素。更加确切地讲,List通常允许满足 e1.equals(e2) 的元素重复加入容器。

​ List接口常用的实现类有3个:ArrayList、LinkedList和Vector。目前我们主要使用ArrayList。其底层实现是数组。LinkedList底层实现是链表。Vector底层实现也是数组,只不过,它是线程安全的。

​ 由于List继承了Collenction这个接口,所以List中的许多方法,用法和Collection一样。

​ 下面是一些LIst接口中,主要是其实现类 ArrayList 的常用方法。

package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.List;/*** 测试List方法*/
public class TestList {public static void main(String[] args) {test03();}public static void preFunction() {Collection<String> c = new ArrayList<>();// 增加元素c.add("钱前");c.add("倩倩");System.out.println(c);System.out.println(c.size());// 元素的大小System.out.println(c.isEmpty());// 元素是否为空System.out.println("===========");// 转化出一个Object[]数组Object[] objs = c.toArray();System.out.println(objs);System.out.println("==========");// 返回是否包含该元素System.out.println(c.contains("倩倩"));System.out.println(c.contains("高老五"));System.out.println("==========");c.remove("倩倩"); // 删除指定对象System.out.println(c);System.out.println("==========");c.clear(); // 删除所有的对象System.out.println(c);System.out.println(c.size());}public static void restFunction() {List<String> list01 = new ArrayList<>();list01.add("aa");list01.add("bb");list01.add("cc");List<String> list02 = new ArrayList<>();list02.add("aa");list02.add("dd");list02.add("ee");System.out.println("list01: " + list01);System.out.println("===========");// 传入对象的所有的元素加入原数组list01.addAll(list02);// 将两个容器中相同的对象删除list01.removeAll(list02);// 输出两个容器中相同对象list01.retainAll(list02);System.out.println("list01: " + list01);System.out.println("===========");System.out.println(list01.containsAll(list02));}public static void test03() {List<String> list = new ArrayList<>();list.add("A");list.add("B");list.add("C");list.add("D");System.out.println(list);System.out.println("===========");// 在索引位置插入一个对象list.add(2, "钱前");System.out.println(list);System.out.println("===========");// 删除对应索引的对象list.remove(2);System.out.println(list);System.out.println("===========");// 将对应索引值的对象更改为传入的对象list.set(2, "钱前");System.out.println(list);System.out.println("===========");// 获得对应索引对象System.out.println(list.get(2));System.out.println("===========");// 返回传入对象的第一个索引位置,没有返回-1System.out.println(list.indexOf("钱前"));System.out.println(list.indexOf("倩倩"));System.out.println("===========");// 返回传入对象的最后一个索引位置,没有返回-1list.add("A");list.add("B");list.add("C");list.add("D");System.out.println(list);System.out.println(list.lastIndexOf("B"));System.out.println("===========");}
}// 这是结果:
/*
* [A, B, C, D]
* ===========
* [A, B, 钱前, C, D]
* ===========
* [A, B, C, D]
* ===========
* [A, B, 钱前, D]
* ===========
* 钱前
* ===========
* 2
* -1
* ===========
* [A, B, 钱前, D, A, B, C, D]
* 5
* ===========
*/

2.2 ArrayList的底层实现

​ ArrayList底层使用数组实现的存储。特点:查询效率高,增删效率低,线程不安全。我们一般使用它。

​ 如果你的程序涉及到大量的增删处理,可以使用LInkedList。如果需要考虑线程安全性,那么使用Vector。

​ 数组长度是有限的,而ArrayList是可以存放任意数量的对象,长度不限,那么他是怎么实现的呢?

​ 它使用数组扩容来实现的。

​ 底层默认的数组长度是10,如果存入的值存满默认长度数组还不够,就将创建一个新的数组来进行扩容,将旧数组中的所有对象存入新数组,并接着依次存入剩下的对象。一般而言一次扩容的长度是底层默认数组长度的一半。如果,扩容之后的数组大小依然不够,那么,继续扩容,直到将传入的对象全部装入为止。

​ 其他的,如删除插入都是数组的拷贝。clear方法一次清空,就是依次将数组中的值置为null等。其实ArrayList的底层实现并不难。

下面自己写的一段ArrayList的简单底层代码实现:

package com.test.mycollection;/*** 自定义试验一个ArrayList,体验底层原理*/
public class QQArrayList<E> {// 底层是一个Object对象数组private Object[] elementData;// 这个是一个对象数组的长度private int size = 0;// 默认数组长度private static final int DEFAULT_CAPACITY = 10;// 默认构造函数,如果不指定长度,那么数组设置为默认长度10public QQArrayList(){elementData = new Object[DEFAULT_CAPACITY];}// 如果输入长度,进行判断,然后根据相应的情况赋值或者抛出异常public QQArrayList(int capacity){if(capacity < 10) {throw new  RuntimeException("容器不能为负");}else if(capacity == 0) {elementData = new Object[DEFAULT_CAPACITY];}else{elementData = new Object[capacity];}}// 由于object的默认toString方法是不会一个一个输出数据的。// 所以我们需要重写toString方法以便我们输出。// 使用StringBuilder类@Overridepublic String toString(){StringBuilder sb = new StringBuilder();sb.append("[");for(int i = 0; i < size; i++) {sb.append(elementData[i] +",");}sb.setCharAt(sb.length() - 1, ']');return sb.toString();}// 这里就是整个ArrayList底层最难的地方,这里需要对长度不足的数据进行扩容。public void add(E element) {// 什么时候扩容?当size等于数组长度时,就需要扩容if(size == elementData.length){Object[] newArray = new Object[elementData.length + (elementData.length >> 1)]; // 右移1位,相当于 / 2// 怎么扩容?将原来数组中的数据拷贝到新数组中,并将新数组的地址值赋值给旧数组System.arraycopy(elementData, 0, newArray, 0, elementData.length);elementData = newArray;}elementData[size++] = element;}public E get(int index) {// 返回索引的值checkRange(index);return (E) elementData[index];}public void set(E element, int index) {// 根据索引修改static值checkRange(index);elementData[index] = element;}public void checkRange(int index) {// 索引合法判断if(index < 0 || index > size -1) {// 不合法throw new RuntimeException("索引不合法: " + index );}}public void remove(E element) {// 将传入的元素和容器内的元素一一比较,获得第一个比较为true的,返回for(int i = 0; i < size; i++) {if(element.equals(get(i))) { // 容器中所有的比较操作都是equals,而不是 ==// equals方法比较内容是否相同,而 == 一般用来比较地址值是否相同//将该元素从此处移除remove(i);}}}public void remove(int index) {checkRange(index);int numberMove = elementData.length-index-1;System.arraycopy(elementData, index + 1, elementData, index, numberMove);elementData[--size] = null; // --size,先减1,在执行语句。size--,先执行语句再减1}public int size() {return size;}public boolean isEnpty() {if(size == 0) {return true;}else if(size > 0) {return false;}}public static void main(String[] args) {QQArrayList<Integer> Q1 = new QQArrayList<>();for(int i =0; i < 16; i++){Q1.add(i);}System.out.println(Q1.toString());System.out.println(Q1.get(1));Q1.remove(13);System.out.println(Q1.toString());Integer obj = 5;Q1.remove(obj);System.out.println(Q1.toString());System.out.println(Q1.size());System.out.println(Q1.isEnpty());QQArrayList<String> Q2 = new QQArrayList<>();System.out.println(Q2.isEnpty());}}//输出结果
/**
* [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
* 1
* [0,1,2,3,4,5,6,7,8,9,10,11,12,14,15]
* [0,1,2,3,4,6,7,8,9,10,11,12,14,15]
* 14
* false
* true
*/

2.3 LinkedList特点和底层实现

​ LinkedList底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。

​ 双线链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。多疑,双向链表中的人一个节点开始,都可以很方便的找到所有的节点。

[外链图片转存失败(img-y2FLVTuZ-1565413944843)(E:\Document\java\JAVE EE\尚学堂笔记\深度截图_选择区域_20190712160653.png)]

​ 每一个节点包含三个部分:上一个节点,下一个节点,存储的数据。

​ 通过书写简单的LinkedList的底层代码来深刻的了解LinkedList的运行原理:

package com.test.mylinklist;/*** 通过手写来理解LinkedList*/// 首先链表需要自己去定义一个节点
// 这个节点有三个部分,存放前一个节点,存放后一个节点,和存放自己的数据
class Node {Node previous; // 上一个节点Node next; // 下一个节点Object element; // 元素数据public Node(Node previous, Node next, Object element) {this.previous = previous;this.next = next;this.element = element;}public Node(Object element) {this.element = element;}
}
public class MyLinkedList<E> {private Node first; // 第一个节点,便于向后进行查找private Node last; // 最后一个节点private int size; // 定义数组的长度// []// ["a", "b", "c"]public void add(E obj) {Node node = new Node(obj);// 如果一开始,头节点是空,说明尾节点也是空,即该链表没有对象。就将对象放入,同事头尾节点都是该对象if(first == null) {first = node;last = node;}else { // 如果头结点不为空,说明已经有对象在该链表中,那么将新的节点对象的前一个对象指针指向原容器中最后一个对象,并且将新节点重新定义为尾节点。node.previous = last;node.next = null; // 可以不用谢,因为原来即为空last.next = node;last = node;}size ++; // 每次调用完使数组的长度加1}// 根据索引值,插入给定对象public void add(int index, E obj) {checkRange(index); // 首先判断索引值正确与否Node newNode = new Node(obj); // 创建一个新的节点,用来插入Node temp = getNode(index); // 创建一个新的临时节点,用来放入容器中索引值对应的原对象// 这里需要进行判断,根据索引值找到的原元素是否为头或者尾节点if(temp != null) {if(temp.previous == null) {// 如果temp节点为头结点,将temp节点的下一节点置为新节点;新节点的下一个节点置为temp节点,这样就连来了。temp.previous = newNode;newNode.next = temp;first = newNode;}else if(temp.next == null) {// 如果temp节点的下一节点为空,说明temp节点已经是尾节点。这时,将temp节点的下一节点置为新节点;新节点的下一节点置为temp节点,就在temp节点后添加了新的节点。temp.next = newNode;newNode.previous = temp;}else {// 如果temp节点上一节点和下一节点均不为空,说明temp节点在中间。这就需要断开temp节点与前一个节点的联系,并将新的节点与temp的前一节点和temp节点连接起来。其具体操作是上两个方法的综合。Node up = temp.previous;up.next = newNode;newNode.previous = up;newNode.next = temp;temp.previous = newNode;}}}/*** 重写toString方法。因为Object的toString方法只能显示数据的类型和该容器的第一个元素所在的内存空间。* 所以重写toString方法。这里的toString方法,不断遍历数组(使用 temp =temp.next ),并将其不断添加到* 创建的StringBuilder对象,最后返回StringBuilder对象。*/@Overridepublic String toString() {// [a,b,c] first = a, last = c// a,b,cStringBuilder sb = new StringBuilder();sb.append("[");Node temp = first;while(temp!=null) {sb.append(temp.element);sb.append(",");// 那么怎么进行遍历?只需要将temp的下一个节点赋值给temp。temp = temp.next;}sb.setCharAt(sb.length()-1, ']');return sb.toString();}private void checkRange(int index) {if(index < 0 || index > size -1) {throw new RuntimeException("索引数字不合法 : " + index);}}public E get(int index) {checkRange(index);Node temp = getNode(index);return temp != null ? (E) temp.element : null;}// 这个方法会多次使用,所以单独封装private Node getNode(int index) {checkRange(index);Node temp =null;/*** 这里选择优化了一int index下算法,如果数组很长,从头开始,效率很低。这里选择使用了* 一次二分法进行算法的优化。如果大于中间的数,从屁股开始遍历。* 如果小于中间的数,从头开始遍历。* 但是我觉得如果数据比较大的时候,加入二分法是一个不错的考虑*/if(index < (size >> 1)){ // size >>1 相当于除以2temp = first;for(int i=0; i < index; i++) {temp = temp.next;}}else {temp = last;for (int i = size -1; i > index ; i--) {temp = temp.previous;}}return temp;}// 删除指定索引的值,如果是中间元素,只需要将断开其上一个节点和下一节点的联系,并将上下节点重新连接即可。如果是头结点,将下一个节点赋值给头结点。如果是最后一个元素,将前一个元素赋值给尾节点。别忘了,每删除一个元素,就需要将size-1;public void remove(int index) {checkRange(index);Node temp = getNode(index);if(temp != null) {Node up = temp.previous;Node down = temp.next;// 删除中间元素if(up != null) {up.next = down;}if(down != null){down.previous = up;}// 删除第一个元素if(index == 0) {first = down;}// 删除最后一个个元素if(index == size -1) {last = up;}size--;}}// 删除出现的第一个obj元素public void remove(E obj) {Node temp = first;int count = 0;while(temp != null) {// 通过遍历查找相同的对象,记下索引值,并调用remove(int index)方法进行删除操作。if(temp.element.equals(obj)) {remove(count);break;}count ++;temp = temp.next;}}public static void main(String[] args) {MyLinkedList<String> list = new MyLinkedList<>();list.add("c");list.add("b");list.add("d");list.add("c");list.add("c");list.add("e");list.add("c");list.add("c");list.add("f");System.out.println(list.get(5));}}

2.4 Vector向量

​ Vector底层使用数据实现的List,相关方法都加了同步检测,因此"线程安全"。比如,IndexOf方法就添加了synchronize同步标记。

public synchronized int indexOf(Object o, int index) {// 代码
}

建议:

​ 如何选用ArrayList、Link、Vector?

  1. 需要线程安全,用Vector。 —— 安全往往意味着效率低。
  2. 不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)。为什么要一般使用ArrayList呢?因为一般时候,一个ArrayList容器是一个程序片断所私有,一般不会涉及多线程操作,相比起Vector效率又高,所以一般使用ArrayList。
  3. 不存在线程安全问题时,增加或删除元素较多用LinkedList。

3.Map接口

​ 现实中,我们经常需要成对存储某些信息。比如,我们使用的微信,一个手机号只能对应一个微信账户。这就是一种成对存储的关系

​ Map就是用来存储"键(key)-值(value)" 对象的。Map类中存储的"键值对"通过键来标识,所以"键值对"不能重复。

​ Map接口的实现类有HashMap、TreeMap、HashTable、Properties等。

​ Map接口中常用的方法:

方法 说明
Object put(Object key, Object value) 存放键值对
Object get(Object key) 通过键对象查找得到值对象
Object remove(Object key) 删除键对象对应的键值对
boolean containsKey(Object key) Map容器中是否包含键对象对应的键值对
boolean containsVaule(Object value) Map容器中是否包含值对象对应的键值对
int size() 包含键值对的数量
boolean isEmpty() Map是否为空
void putAll(Map t) 将t的所有键值对存放到本map对象中
void clear() 清空本map对象所有键值对

​ 下面是一段Map用法的代码:

package collection;import java.util.HashMap;
import java.util.Map;/*** 测试HashMap*/
public class TestMap {public static void main(String[] args) {Map<Integer,String> m1 = new HashMap<Integer, String>();m1.put(1, "钱前");m1.put(2, "张石");m1.put(3, "李师傅");System.out.println(m1);System.out.println(m1.get(1));System.out.println(m1.size());System.out.println(m1.containsKey(2));System.out.println(m1.containsValue("王司徒"));Map<Integer,String > m2 = new HashMap<>();m2.put(4,"Four");m2.put(5, "Five");m2.putAll(m1);System.out.println(m2);// map中键不能重复,如果重复(根据equals方法),新的覆盖旧的m1.put(3, "渔船风");System.out.println(m1);}}

​ 第二个Map例子:

package collection.hashmap;import java.util.HashMap;
import java.util.Map;public class TestMap2 {public static void main(String[] args) {Employee e1 = new Employee(1001, "高淇", 50000);Employee e2 = new Employee(1002, "高淇2", 60000);Employee e3 = new Employee(1003, "高淇3", 70000);Map<Integer, Employee> map = new HashMap<>();map.put(1001, e1);map.put(1002, e2);map.put(1003, e3);Employee emp = map.get(1001);System.out.println(emp.toString());}}class Employee {private int id;private String name;private double salary;public Employee(int id, String name, double salary) {this.id = id;this.name = name;this.salary = salary;}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public double getSalary() {return salary;}public void setSalary(double salary) {this.salary = salary;}@Overridepublic String toString() {return "id: " + id +" " + "name: " + name + " " + "salary: " + salary;}
}

3.1 HashMap底层实现

​ HashMap底层实现用了哈希表,哈希表是一种非常重要的数据结构。对于以后理解很多技术都非常有帮助(比如:redis数据库的核心技术和HashMap一样),因此,非常有必要让大家理解。

​ 数据结构中有数组和链表来实现堆数据的存储,他们各有优点。

​ (1) 数据: 占用空间连续。寻址容易,查询速度快。但是增加和删除效率非常低。

​ (2) 链表: 占用空间不连续。寻址困难,查询速度慢。但是增加和删除效率非常高。

​ 那么,我们能不能结合数据和链表的优点(即查询速度快,增删效率也高)呢?答案就是"哈希表"。哈希表的本质就是"链表加数组"。

重点:

​ 对于本章中频繁出现的"底层实现"讲解,要尽量搞通,以便应对一些大型企业的笔试面试。

3.1.1 HashMap基本结构讲解 ( 这里以JDK1.6中的HashMap为例 )

​ 哈希表的节本结构就是"数据+链表"。这是其中一段代码:

public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {// 核心数组默认初始化的大小为16(数组大小必须为2的整数幂)static final int DEFAULT_INITIAL_CAPACITY = 16;// 负载因子(核心数据被占用超过0.75,则开始启动扩容)static final float DEFAULT_LOAD_FACTOR = 0.75F;// 核心数组(根据需要可以扩容)。数组的长度必须始终为2的整数幂。transient Entry[] table; // Entry在父类Map.class中// 这里是JDK1.6的代码,JDK1.8.212有所不同。在jDK1.8.212中这一段代码为:// transient HashMap.Node<K, V>[] table;
}

​ 其中的Entry[] table 就是HashMap的核心数组结构,我们也称之为"位桶数组"。我们继续看Entry是什么。源码如下:

// JDK1.6中的源码:
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;}// 以下代码省略
}// 一下代码省略,同时这里是JDK1.8.212的源码。
static class Node<K, V> implements Entry<K, V> {final int hash;final K key;V value;HashMap.Node<K, V> next;Node(int var1, K var2, V var3, HashMap.Node<K, V> var4) {this.hash = var1;this.key = var2;this.value = var3;this.next = var4;}}

​ 一个Entry对象存储了:

  1. key:键对象 value:值对象
  2. next:下一个节点
  3. hash:键对象的hash值

​ 显然每一个Entry对象就是一个单向链表结构,我们使用图像表示一个Entry对象的典型示意:

​ 然后,我们画出Entry[](JDK1.8.212中为Node<K, V>[])的结构,也就是HashMap的结构:

3.2.1 存储数据过程put(key, value>)

​ 明白了HashMap的基本结构后,我们继续深入学习HashMap如何存储数据。此处的核心是如何产生hash值,该值用来对应数组的存储位置。

​ 我们的目的是将"key-value两个对象"成对存放到HashMap的Entry[]数组中。参见以下步骤 ( 这里以JDK1.6的源码为例,在JDK1.8中,Entry对象更改为 Node ) :

(1) 获得key对象的hashcode

​ 首先调用key对象的hashcode()方法,获得hashcode。

(2) 根据hashcode计算出hash值 ( 要求在[ 0, 数据长度-1 ] 区间 )

​ hashcode是一个整数,我们需要将它转化成 [ 0, 数组长度-1 ] 的范围。我们要求转化后的hash值尽量均匀地分布在 [ 0, 数组长度-1 ] 这个区间,减少 “hash冲突”:

​ i. 一种极端简单和底下的算法是:

​ hash值 = hashcode/hashcode;

​ 也就是说,hash值总是1。意味着,键值对对象都会存储到数据索引1位置,这样就形成了一个非常长的链表。相当于每存储一个对象都会发生"hash冲突",HashMap也退化成了一个"链表"。

​ ii. 一种简单和常用的算法是 ( 相除取余算法 ):

​ hash值 = hashcode % 数据长度

​ 这种算法可以让hash值均匀地分布在 [ 0, 数组长度-1 ] 这个区间。早期的 HashTable 就是采用这种算法。但是,这种算法由于使用了 " 除法 ",效率底下。JDK后来改进了算法。首先约定数据长度必须为2的整数幂,这样采用位运算即可实现取余的效果:hash值 = hashcode & ( 数组长度-1 )。

​ iii. 如下为我们自己测试简单的hash算法:

package collection.hashmap;/*** 测试一下hash算法:相除取余数算法*/public class TestHash {public static void main(String[] args) {int h = 254854603;int length = 16; // length 为 2 的整数次幂,则 h & (length - 1) 相当于堆length取模myHash(h, length);}public static int myHash(int h, int length) {System.out.println(h & (length - 1));// length为2的整数次幂情况下,和取余的值一样System.out.println(h % length); // 取余数return h & (length - 1);}}/**
* 结果为11
*/

​ 运行如上程序,我们就能发现直接取余 ( h % length ) 和 ( h & ( length -1 ) ) 结果是一致的。事实上,为了获得更好的散列效果,JDK 对 hashcode 进行了两次散列处理( 核心目标就是为了分布更散更均匀 ),源码如下 ( 此源码为JDK1.6中的源码,而在JDK1.8.212 中,源码又进行了优化 ):

static int hash(int h) {// This function ensures that hashCode that differ only by// onstant multiples at each bit position hava a bounded // number of collisions (approximately 8 at default load factor).h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {return h & (length -1);
}

​ 而在JDK1.8.212中,其算法如下:

static final int hash(Object var0) {int var1;return var0 == null ? 0 : (var1 = var0.hashCode()) ^ var1 >>> 16;
}

​ 上述代码会直接获取传入对象的hashcode。而hashCode() 这个函数定义在Object中,使用Integer将地址值转化成Integer对象。

(3) 生成Entry对象

​ 如上所述,一个Entry对象包含四个部分:key对象、value对象、hash值、指向下一个Entry对象的引用。我们现在算出了hash值。下一个Entry对象的引用为null。

(4) 将Entry对象放到table数组中。

​ 如果Entry对象对应的数组索引位置还没有放Entry对象,则直接将Entry对象存储进数组;如果对应索引位置已经有Entry对象,则将已有Entry对象的next指针指向本Entry对象,形成链表。

总结如上过程

​ 当添加一个元素 (key-value) 时,首先计算key的hash值,一次确定插入数组中的位置,但是可能存在同一个hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,它们在数组的同一位置,这样就形成了链表,同一个链表上的hash值是相同的,所以说数组存放的是链表。

​ JDK8中,当链表长度大于8时,链表就转化为了红黑树,这样又大大提高了查找效率。

3.2.2 取数据过程get(key)

​ 我们需要通过key对象获得 “键值对” 对象,进而返回value对象。明白了存储数据过程,取数据就比较简单了,参见以下步骤:

​ (1) 获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数据的位置。

​ (2) 在链表上挨个比较key对象。调用equals()方法,将key对象和链表上所有节点的key对象进行比较,直到碰到返回true的节点对象为止。

​ (3) 返回equals() 为true的节点对象的value对象。

​ 明白了存取数据的过程,我们再来看一 || size > elementData.length下 hashcode() 和 equals() 方法的关系:

​ Java中规定,两个内容相同 (equals()为true) 的对象必须具有相等的hashcode。因为如果equals() 为true而两个对象的hashcode不同,那么整个存储过程就发生了悖论。

​ 什么意思呢?意思就是,只有hashcode相同,我们在第一步取hashcode,第二步根据hashcode寻找数组中对应的索引位置时,才能找到。而如果hashcode不同,就意味着两个内容相同的对象却不存放在统一索引位置,那么存取过程就会发生混乱。

3.2.3 扩容问题

​ HashMap的位桶数组,初始大小为16。实际使用时,显然大小是可变的。如果位桶数组中的元素达到 (0.75 * 数组长度),就重新调整数组大小,使其变为原来的两倍大小。

​ 扩容很耗时。扩容的本质是定义新的更大的数组,并将就数组内容挨个拷贝到新数组中。

3.2.4 JDK8 将链表在大于 8 情况下变为黑红二叉树

​ JDK8中,HashMap在存储一个元素时,当对应链表长度大于8时,链表就转换为黑红树,这样又大大提高了查找的效率。

3.2.5 通过编写自定义的HashMap来理解HashMap底层实现

package collection.hashmap.myhashmap;/*** 通过自定义一个HashMap来理解HashMap的底层原理* 第一步:实现put方法增加键值对,并解决了键重复的时候覆盖相应节点* 第二步:重写toString() 方法,方便查看map中的键值对* 第三步:增加get()方法,根据键对象,获取键值对* 第四步:增加泛型*//*** 用于MyHashMap*/
class Node<K, V> {int hash;K key;V value;Node next;}public class MyhashMap<K, V> {// 位桶数组。 bucket arrayNode<K, V>[] table;// 存放键值对的个数int size;// 负载因子static final float DEFAULT_LOAD_FACTOR = 0.75F;public MyhashMap() {// 默认长度,2的整数次幂table = new Node[16];}@Overridepublic String toString() {// ( 10:aa, 20:bb )StringBuilder sb = new StringBuilder("{ ");for (int i = 0; i < table.length; i++) {Node temp = table[i];while (temp != null) {sb.append(temp.key + ": " + temp.value + "," + " ");temp = temp.next;}}sb.deleteCharAt(sb.length() - 2);sb.append("}");return sb.toString();}public void put(K key, V value) {// 定义了新的节点对象Node<K, V> newNode = new Node<>();newNode.hash = myHash(key.hashCode(), table.length);newNode.key = key;newNode.value = value;newNode.next = null;Node<K, V> temp = table[newNode.hash];Node<K, V> iterLast = null; //正在遍历的最后一个元素boolean keyReapt = false;if (temp == null) {// 此处数组元素为空,则直接将新节点放进去table[newNode.hash] = newNode;size++;} else {// 此处数组不为空,则遍历对应链表。while (temp != null) {// 判断key,如果重复,则覆盖if (temp.key.equals(key)) {int arrayLength = 0;keyReapt = true;// 只是覆盖value即可,其他值保持不变。其他值(hash, next, previous)temp.value = newNode.value;arrayLength++;break;} else {int arrayLength = 0;// key不重复,则遍历下一个iterLast = temp;temp = temp.next;arrayLength++;// 什么时候扩容if (arrayLength == table.length * DEFAULT_LOAD_FACTOR) {Node[] newArray = new Node[table.length + (table.length >> 1)];// 怎么扩容System.arraycopy(table, 0, newArray, 0, table.length);table = newArray;}}}if (keyReapt == false) { // 如果没有发生key重复的情况,则添加到最后。iterLast.next = newNode;size++;}}}public V get(K key) {int hash = myHash(key.hashCode(), table.length);V value = null;if (table[hash] != null) {Node<K, V> temp = table[hash];while (temp != null) {if (temp.key.equals(key)) { // 如果相等,说明找到了相应的键值对value = temp.value;break;} else {temp = temp.next;}}}return value;}public int myHash(int v, int length) {// 直接按位运算,效率高
//        System.out.println("hash in myHash:" + (v&(length -1)));// 取模运算,效率较低
//        System.out.println("hash in myHash:" + (v%(length -1)));return v & (length - 1);}public static void main(String[] args) {MyhashMap<Integer, String> map1 = new MyhashMap<>();map1.put(10, "aa");map1.put(20, "bb");map1.put(30, "cc");map1.put(40, "dd");map1.put(50, "ee");System.out.println(map1.toString());System.out.println(map1.get(40));}}

3.2 TreeMap的使用和底层实现

​ TreeMap是红黑二叉树的典型实现。我们打开TreeMap的源码,返现里面有一行核心代码:

private transient Entry<K, V> root = null;

​ root用来存储整个树的根节点。我们继续跟踪Entry ( 是TreeMap的内部类 ) 的代码:

static final class Entry<K, V> implements Map.Entry<k, V>{K key;V value;Entry<K, V> left = null;Entry<K, V> right = null;Entry<K, V> parent;boolean color = BLACK;
}

​ 我们可以看到里面存储了本身数据、做节点、右节点、父节点、以及节点颜色。TreeMap的put()/remove() 方法大量使用了红黑树的理论。

​ TreeMap和HashMap实现了同样的接口,因此,用法对于调用者来讲没有什么不同。HashMap效率高于TreeMap。在需要排序的Map时才使用TreeMap。

使用 Comparable() 这个类来实现自定义排序

​ TreeMap在使用时,会自动根据按照key递增的方式排序。具体如何排序,是按照comparable()这个类来实现的。

​ 具体的用法

class Employee implements Comparable<Employee> {int id;String name;double salary;public Employee(int id, String name, double salary) {this.id = id;this.name = name;this.salary = salary;}@Overridepublic int compareTo(Employee employee) {if (this.salary > employee.salary) {return 1;} else if (this.salary < employee.salary) {return -1;} else {if (this.id > employee.id) {return 1;} else if (this.id < employee.id) {return -1;} else {return 0;}}}
}

3.3 HashMap和HashTable

​ HashMap采用哈希算法实现,是Map接口最常用的实现类。由于底层才用了哈希表存储数据,我们要求键不能重复,如果发生重复,新的键值对会替换就得键值对。HashMap在查找、删除、修改方面都有着非常高的效率。

​ HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。

HashMap与HashTable的区别:

  1. HashMap:线程不安全,效率高。允许key或value为null。
  2. HashTable:线程安全,效率低。不允许key或value为null。

4.Set接口

​ Set接口继承字Collection,Set接口中没有新增方法,方法和Collection保持完全一致。我们在前面通过List学习的方法,在Set中仍然适用。因此,学习Set的适用没有难度。

​ Set容器特点:无序、不可重复。无序指Set中的元素没有索引,我们只能遍历查找;不可重复指不允许加入重复的元素。更确切地讲,新元素如果和Set中,某个元素通过equals() 方法对比为true,则不能加入;甚至,Set中也只能放入一个null元素,不能多个。

​ Set常用的实现类有:HashSet、TreeSet等,我们一般使用HashSet。

4.1 HashSet基本使用

​ 重点体会"Set是无序、不可重复"的核心要点。

HashSet的使用

import java.util.HashSet;
import java.util.Set;public class TestHashSet {public static void main(String[] args) {Set<String> set = new HashSet<>();// 特性:不可重复set.add("aa");set.add("bb");set.add("aa");System.out.println(set);set.remove("bb");System.out.println(set);Set<String> set2 = new HashSet<>();set2.add("高淇");set.addAll(set2);System.out.println(set);}
}

4.2 HashSet的底层实现

​ HashSet是采用哈希算法实现,底层实际是使用HashMap实现的 ( HashSet本质就是一个简化版的HashMap ),因此,查询效率和增删效率都比较高。这里是HashSet的源码:

public class HashSet<E> implements Set<E>, Cloneable, java.io.Serializable {private transient HashMap<E, Object> map; // 如果想往HashSet对象存放元素,都是存放在map,这个HashMap对象中。Set元素中的所有元素都在这个map对象中作为key键的。而value是下面定义的PRESENT对象。private static final Object PRESENT = new Object();public HashSet() {map = new HashMap<E, Object>();}public boolean add(E e) {return map.put(e, PRESENT) == null;}// 一下代码省略
}

​ 我们发现里面有一个map属性,这就是HashSet的核心秘密。我们再看 add() 方法,发现增加一个元素说白了就是在map中增加一个键值对,键对象就是这个元素,值对象是名为PRESENT的Object对象。

​ 所以,我们在使用HashSet,使用add() 方法向其中添加元素时。由于我们添加的元素是作为HashSet底层中的HashMap的key,而key不可以重复。这就是Set不能重复的原因。

4.3 TreeSet的使用和底层实现

​ TreeSet底层实现实际使用TreeSet实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。

​ TreeSet的使用方法和Set方法一致。

5.迭代器介绍

​ 关于迭代器的使用:

import java.util.*;public class TestIterator {public static void main(String[] args) {testIteratorList();testIteratorSet();testIteratorMap();testIteratorMap2();}// 使用迭代器遍历Listpublic static void testIteratorList() {List<String> list = new ArrayList<>();list.add("aa");list.add("bb");list.add("cc");for (Iterator<String> iter = list.iterator(); iter.hasNext(); ) {String temp = iter.next();System.out.println(temp);}}// 遍历Setpublic static void testIteratorSet() {Set<String> set = new HashSet<>();set.add("aa");set.add("bb");set.add("cc");for (Iterator<String> iter = set.iterator(); iter.hasNext(); ) {String temp = iter.next();System.out.println(temp);}}// 遍历mappublic static void testIteratorMap() {Map<Integer, String> map = new HashMap<>();map.put(100, "a0");map.put(110, "a1");map.put(150, "a2");Set<Map.Entry<Integer, String>> ss = map.entrySet();for(Iterator<Map.Entry<Integer, String>> iter = ss.iterator(); iter.hasNext(); ) {Map.Entry<Integer, String> temp =  iter.next();System.out.println(temp.getKey() + "--" + temp.getValue());}}public static void testIteratorMap2() {Map<Integer, String> map = new HashMap<>();map.put(100, "a0");map.put(110, "a1");map.put(150, "a2");Set<Integer> keySet = map.keySet();for (Iterator<Integer> iterator = keySet.iterator(); iterator.hasNext(); ) {Integer key = iterator.next();System.out.println(key + "--" + map.get(key));}}}s

5.1 遍历集合的方法总结

​ 遍历List方法一:普通for循环

​ 遍历List方法二:增强for循环 ( 使用泛型! )

​ 遍历List方法三:使用迭代器(1)

for(Integer iter = list.iterator();iter.hasNext();){String temp = (String) iter.next();System.out.println(temp);
}

​ 遍历List方法四:使用迭代器(2)

Iterator iter = list.iterator();
while(iter.hasNext()) {Object obj = iter.next();iter.remove(); // 如果要遍历时,删除集合中的元素,建议使用这种方式。System.out.println(obj);
}

​ 遍历Set方法一:增强for 循环

for(string temp:set) {System.out.println(temp);
}

​ 遍历Set方法二:使用Iterator迭代器 (1)

for(Iterator iter = set.iterator();iter.hasNext();) {String temp = (String) iter.next();System.out.println(temp);
}

​ 遍历Map方法一:根据key获取value

Map<Iterator, Map> maps = new HashMap<Integer, Man>();
Set<Integer> keySet = maps.keySet();
for(Integer id : keySet) {System.out.println(maps.get(id).name);
}

​ 遍历Map方法二:使用entrySet

Set<Map.Entry<Integer, Man>> ss = maps.entrySet();
for(Iterator iterator = ss.iterator(); iterator.next();) {Map.Entry e = (Entry) iterator.next();System.out.println(e.getKey() + " " + e.getValue());
}

6.Collections工具类

​ 类java.util.Collections提供了堆Set、List、Map进行排序、填充、查找元素的辅助方法。

  1. void sort(List):对List容器内的元素排序,排序的规则是按照升序进行排序。
  2. void shuffle(List) :对List容器内的元素进行随机排列。
  3. void reverse(List):对List容器内的元素进行逆序排列。
  4. void fill(List, Object) :用一个特定的对象重写整个List容器。
  5. int binarySearch(List, Object):对于顺序的List容器,采用折半查找的方法查找特定对象。

import java.util.ArrayList;import java.util.Collections;import java.util.List;/*** 辅助类的使用*/
public class TestCollections {public static void main(String[] args) {List<String> list = new ArrayList<>();for (int i = 0; i < 10; i++) {list.add("钱" + i);}System.out.println(list);Collections.shuffle(list); // 随机排列System.out.println(list);Collections.reverse(list); // 逆序排列System.out.println(list);Collections.sort(list); // 按照递增的方式排序  自定义类使用Comparable接口System.out.println(list);System.out.println(Collections.binarySearch(list, "钱7")); // 二分法查找}
}

7.使用容器来存储表格

ID 姓名 薪水 入职日期
1001 张三 20000 2018.5.5
1002 李四 200000 2005.4.4
1003 王五 3000 2019.7.2

想法1:

​ 每一行数据使用Map

​ 整个表格使用list

​ 这个其实就是ORM思想(对象关系映射)

import java.util.*;/*** 测试表格的存储* ORM思想的简单实现:map表示一行数据,多行数据就是多个map,再将map放入List中*/public class TestStoreData {public static void main(String[] args) {Map<String, String> row1 = new HashMap<>();row1.put("id", "1001");row1.put("name", "张三");row1.put("薪水", "20000");row1.put("入职日期", "2018.5.5");Map<String, String> row2 = new HashMap<>();row2.put("id", "1002");row2.put("name", "李四");row2.put("薪水", "200000");row2.put("入职日期", "2005.4.4");Map<String, String> row3 = new HashMap<>();row3.put("id", "1002");row3.put("name", "王五");row3.put("薪水", "3000");rowjavajava3.put("入职日期", "2019.7.2");List<Map<String, String>> table1 = new ArrayList<>();table1.add(row1);table1.add(row2);table1.add(row3);for(Map<String, String> row: table1) {Set<String> keySet = row.keySet();for(String key: keySet) {System.out.print(key + ": " + row.get(key) + "\t");}System.out.println();}}}

想法二:

​ 每一行使用一个:javabean对象。

​ 整个表格使用一个Map/List

import java.util.List;
import java.util.ArrayList;/*** 测试表格数据的存储* 体会ORM思想* 每一行数据使用Javabean对乡村粗,多行使用放入map或者list中*/public class UseJavaBean {public static void main(String[] args) {User user1 = new User(1001, "张三", 20000, "2018.5.5");User user2 = new User(1002, "李四", 200000, "2005.4.4");User user3 = new User(1003, "王五", 3000, "2019.7.2");List<User> list = new ArrayList<>();list.add(user1);list.add(user2);list.add(user3);for (User u : list) {System.out.println(u);}}}class User {private int id;private String name;private double salary;private String hireData;// 一个完整的javabean,要有Set和Get方法,以及无参构造方法public User() {}public User(int id, String name, double salary, String hireData) {this.id = id;this.name = name;this.salary = salary;this.hireData = hireData;}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public double getSalary() {return salary;}public void setSalary(double salary) {this.salary = salary;}public String getHireData() {return hireData;}public void setHireData(String hireData) {this.hireData = hireData;}@Overridepublic String toString() {return "id: " + id + " " + "name: " + name + " " + "salary: "+ salary + " " + "hiredate: " + hireData;}
}

尚学堂视频笔记三:容器相关推荐

  1. 正则表达式基础知识---文本操作(尚学堂视频笔记)

    正则表达式(Regular Expression)基础知识 一.开发中使用流程: -分析要匹配的数据 写出测试用的典型数据 -在工具软件中进行匹配测试 -在程序中调用通过测试的正则表达式 (有些高级语 ...

  2. 尚学堂学习笔记。。。

    1.02_尚学堂马士兵_Struts2_Struts2_HelloWorld_2.avi 指定Tomcat的目录,指定JDK搭建开发环境(拷贝jar包,复制struts.xml文件 此文件不要放在WE ...

  3. 看尚学堂视频Java学习笔记

    //这是我刚接触java时做的笔记, 后面也没去整理, 可能其中有些理解有误, 仅供参考!!! 1.java中一个英文字母或一个中文汉字都是2个字节来存储.如:(char是16bit) 2." ...

  4. [React] 尚硅谷 -- 学习笔记(三)

    第三章 react应用(基于react脚手架) 使用create-react-app创建react应用 react脚手架 xxx 脚手架:用来帮助程序员快速创建一个基于 xxx 库的模板项目 包含了所 ...

  5. 尚学堂oracle笔记1

    1.解锁scott用户:  alter user scott account unlock; I.在其他用户切换到sysdba用户   conn sys/root as sysdba II.授权  g ...

  6. 尚学堂Oracle笔记整理

    1.解锁scott用户:   alter user scott account unlock; I.在其他用户切换到sysdba用户   conn sys/root as sysdba II.授权   ...

  7. 尚学堂oracle笔记2

    第一课:客户端           1. Sql Plus(客户端),命令行直接输入:sqlplus,然后按提示输入用户名,密码.           2. 从开始程序运行:sqlplus,是图形版的 ...

  8. 音视频笔记-----三种数字调制的形式之FSK

    数字调制技术 数字调制是指用数字数据调制模拟信号,主要有三种形式:移频键控法FSK(Frequency-shift keying).移幅键控法ASK().移相键控法PSK().计算机在处理信息时,只能 ...

  9. 尚学堂Java学习笔记

    尚学堂Java学习笔记 ============================ J2SDK&JRE J2SDK:JAVA2 SOFTWARE DEVELOPMENT KIT JRE:JAVA ...

  10. 尚学堂Java培训:JAVA优秀书籍推荐

    转自:[http://www.bjsxt.com/books/goodbooks.html] 如果你曾经尝试过自学某些知识点,比如JavaSE.JDBC等等,相信有很多情况会觉得按照书上的操作非常难进 ...

最新文章

  1. android常见错误-Installation error: INSTALL_FAILED_INSUFFICIENT_STORAGE
  2. Python3 django2.0 字段加密 解密 AES
  3. boost::spirit模块实现一个类似于 XML 的小型解析器的测试程序
  4. 再见 Typora,这款 Markdown 编辑器开源又免费!
  5. 从0到1详解数据挖掘过程
  6. 哈工大女孩学计算机毕业转行,2020考生切记,上985读这些专业,据说都是“坑”!...
  7. 数独动态解题演示小网站 - 基于Vue/pixi.js/Flask
  8. 查看一个进程的线程情况
  9. LeetCode 412 Fizz Buzz
  10. 手把手教你上架HarmonyOS(鸿蒙)应用
  11. 正则表达式美元符号$
  12. windows平台Emacs单实例原理、设置及右键菜单的添加
  13. 【毕业设计】 大数据二手房数据爬取与分析可视化 -python 数据分析 可视化
  14. html怎么读取lrc文件,lrc文件怎么打开?lrc是什么文件?
  15. django+vue+nginx+frp搭建漫画网站之接入谷歌统计和百度统计(三)
  16. ZCMU-1635:超大型 LED 显示屏(细节题)
  17. 大物下学期期末复习笔记
  18. DNS-over-HTTPS 的下一代是 DNS ON BLOCKCHAIN
  19. 计算机入会大会新生发言稿,新生大会发言稿(精选7篇)
  20. Vue3集成富文本编辑器TinyMce6

热门文章

  1. shell脚本-md5码
  2. 企业邮件服务器哪个好?常用邮箱客户端是哪个?
  3. windows端口配置
  4. gitlab安装配置
  5. 一次看过瘾!中国摩博会的“钢铁怪兽”你最爱哪辆?
  6. 想要体验《失控玩家》里Guy的视角,299美元还远远不够
  7. android获取本地连接ip地址,参照第二步将本地连接改成自动获取IP地址即可
  8. 你不需要完美-你需要的是行动与完成
  9. 交大网院计算机第五次作业答案,2015交大网院计算机第三次作业word操作题
  10. MHL中的packedpixel概念