Java数据类型

Java 数据结构

1.抽象数据类型(ADT)

1.1 表 ADT

1.2 Stack栈 ADT

1.3 Queue队列 ADT

2.集合框架

img

从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

**接口:**是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象

**实现(类):**是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。

**算法:**是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。

2.1集合实现类(集合类)

Java提供了一套实现了Collection接口的标准集合类。其中一些是具体类,这些类可以直接拿来使用,而另外一些是抽象类,提供了接口的部分实现。

标准集合类汇总于下表:

序号类描述
1AbstractCollection 实现了大部分的集合接口。
2AbstractList 继承于AbstractCollection 并且实现了大部分List接口。
3AbstractSequentialList 继承于 AbstractList ,提供了对数据元素的链式访问而不是随机访问。
4LinkedList 该类实现了List接口,允许有null(空)元素。主要用于创建链表数据结构,该类没有同步方法,如果多个线程同时访问一个List,则必须自己实现访问同步,解决方法就是在创建List时候构造一个同步的List。例如:List list=Collections.synchronizedList(newLinkedList(...));LinkedList 查找效率低。
5ArrayList 该类也是实现了List的接口,实现了可变大小的数组,随机访问和遍历元素时,提供更好的性能。该类也是非同步的,在多线程的情况下不要使用。ArrayList 增长当前长度的50%,插入删除效率低。
6AbstractSet 继承于AbstractCollection 并且实现了大部分Set接口。
7HashSet 该类实现了Set接口,不允许出现重复元素,不保证集合中元素的顺序,允许包含值为null的元素,但最多只能一个。
8LinkedHashSet 具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。
9TreeSet 该类实现了Set接口,可以实现排序等功能。
10AbstractMap 实现了大部分的Map接口。
11HashMap HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。 该类实现了Map接口,根据键的HashCode值存储数据,具有很快的访问速度,最多允许一条记录的键为null,不支持线程同步。
12TreeMap 继承了AbstractMap,并且使用一颗树。
13WeakHashMap 继承AbstractMap类,使用弱密钥的哈希表。
14LinkedHashMap 继承于HashMap,使用元素的自然顺序对元素进行排序.
15IdentityHashMap 继承AbstractMap类,比较文档时使用引用相等。

在前面的教程中已经讨论通过java.util包中定义的类,如下所示:

序号类描述
1Vector 该类和ArrayList非常相似,但是该类是同步的,可以用在多线程的情况,该类允许设置默认的增长长度,默认扩容方式为原来的2倍。
2Stack 栈是Vector的一个子类,它实现了一个标准的后进先出的栈。
3Dictionary Dictionary 类是一个抽象类,用来存储键/值对,作用和Map类相似。
4Hashtable Hashtable 是 Dictionary(字典) 类的子类,位于 java.util 包中。
5Properties Properties 继承于 Hashtable,表示一个持久的属性集,属性列表中每个键及其对应值都是一个字符串。
6BitSet 一个Bitset类创建一种特殊类型的数组来保存位值。BitSet中数组大小会随需要增加。

2.2如何使用迭代器

通常情况下,你会希望遍历一个集合中的元素。例如,显示集合中的每个元素。一般遍历数组都是采用for循环或者增强for,这两个方法也可以用在集合框架,但是还有一种方法是采用迭代器遍历集合框架,它是一个对象,实现了Iterator 接口或ListIterator接口。迭代器,使你能够通过循环来得到或删除集合的元素。ListIterator 继承了Iterator,以允许双向遍历列表和修改元素。

遍历 ArrayList:

import java.util.*;
 
public class Test{
 public static void main(String[] args) {
     List<String> list=new ArrayList<String>();
     list.add("Hello");
     list.add("World");
     list.add("HAHAHAHA");
     //第一种遍历方法使用 For-Each 遍历 List
     for (String str : list) {            //也可以改写 for(int i=0;i<list.size();i++) 这种形式
        System.out.println(str);
     }
 
     //第二种遍历,把链表变为数组相关的内容进行遍历
     String[] strArray=new String[list.size()];
     list.toArray(strArray);
     for(int i=0;i<strArray.length;i++) //这里也可以改写为  for(String str:strArray) 这种形式
     {
        System.out.println(strArray[i]);
     }
     
    //第三种遍历 使用迭代器进行相关遍历
     
     Iterator<String> ite=list.iterator();
     while(ite.hasNext())//判断下一个元素之后有值
     {
         System.out.println(ite.next());
     }
 }
}

遍历Map集合

import java.util.*;
 
public class Test{
     public static void main(String[] args) {
      Map<String, String> map = new HashMap<String, String>();
      map.put("1", "value1");
      map.put("2", "value2");
      map.put("3", "value3");
      
      //第一种:普遍使用,二次取值
      System.out.println("通过Map.keySet遍历key和value:");
      for (String key : map.keySet()) {
       System.out.println("key= "+ key + " and value= " + map.get(key));
      }
      
      //第二种
      System.out.println("通过Map.entrySet使用iterator遍历key和value:");
      Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
      while (it.hasNext()) {
       Map.Entry<String, String> entry = it.next();
       System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
      }
      
      //第三种:推荐,尤其是容量大时
      System.out.println("通过Map.entrySet遍历key和value");
      for (Map.Entry<String, String> entry : map.entrySet()) {
       System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
      }
    
      //第四种
      System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
      for (String v : map.values()) {
       System.out.println("value= " + v);
      }
     }
}

HashMap 容量问题

  • HashMap 容量为2次幂,是为了数据的的均匀分布,减少hash冲突
    • 毕竟hash冲突越大,代表数组中一个链的长度越大,这样的话会降低hashmap的性能。
    • 根据key的hash确定其在数组的位置时,如果n为2的幂次方,可以保证数据的均匀插入,如果n不是2的幂次方,可能数组的一些位置永远不会插入数据,浪费数组的空间,加大hash冲突
  • 通过 % 求余确定位置性能不如 & 运算。当n是2的幂次方时:hash & (length - 1) == hash % length
    • 这也是扩容2倍的原因,根据hash & oldCap = 1 / 0 确定新的桶位
      • hash & oldCap(初始是16) > 0:第5位是1,被32取模是存在的,于是新桶值为index(扩容前桶值) + oldCap
      • hash & oldCap(初始是16) == 0:第5位是1,取模结果和oldCap相同,桶值不变

Hashmap里的hash方法里用到了扰动函数,目的是为了减少hash冲突。

低高位异或

思路是保留高位和低位特征

Jdk7中的源码

h ^=(h >>> 20)^(h >>> 12); return h ^(h >>> 7)^(h >>> 4);

这两行代码里用了四次**>>>运算,因为>>>就是把低位去掉保留高位。然后高位和低位进行^位运算。这样不管是高位发生变化,还是低位发生变化都会造成其结果的中低位**发生变化。

为什么我们关注其结果的中低位呢,那是因为后面算index的时候,用了h & (length-1),它的意思就是把高位去掉。 如果没有进行扰动计算,当key仅仅发生高位变动的时候就会发生hash冲突,这对Hashmap来说往往是致命的

Jdk8中对扰动计算做了简化,但其思路还是一样的,可能还有个原因,jdk8引入和红黑树,就算hash冲突稍微多点性能也还不错。

(key == null)? 0 :(h = key.hashCode())^(h >>> 16);

HashMap中的loadFactor & threshold

  • loadFactor加载因子

    loadFactor加载因子反映数组(HashMap)存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。

    loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。

    给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

  • threshold

    threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。

说一说 ArrayList 的扩容机制吧

详见笔主的这篇文章:通过源码一步一步分析ArrayList 扩容机制

这里以无参构造函数创建的 ArrayList 为例分析

1. 先来看 add 方法
    /**
     * 将指定的元素追加到此列表的末尾。 
     */
    public boolean add(E e) {
   //添加元素之前,先调用ensureCapacityInternal方法
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //这里看到ArrayList添加元素的实质就相当于为数组赋值
        elementData[size++] = e;
        return true;
    }
2. 再来看看 ensureCapacityInternal() 方法

可以看到 add 方法 首先调用了ensureCapacityInternal(size + 1)

之前不太理解的一段代码:minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

  • 当前对象数组是空,取默认容量/当前所需容量的最大值
   //得到最小扩容量
    private void ensureCapacityInternal(int minCapacity) {
    	//当前对象数组是空,取默认容量/当前所需容量的最大值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 获取默认的容量和传入参数的较大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10。

3. ensureExplicitCapacity() 方法

如果调用 ensureCapacityInternal() 方法就一定会进过(执行)这个方法,下面我们来研究一下这个方法的源码!

  //判断是否需要扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }

HashSet如何检查重复

  • 先进行hashcode的比较,hashcode不同,没有重复出现
  • hashcode相同,再比较equals(equals true是判定对象相等的唯一条件)

HashSet实现

  • 基本调用HashMap接口

  • //Dummy value to associate with an Object in the backing Map
    //translate 与备份映射中的对象关联的虚拟值
    private static final Object PRESENT = new Object();
    
  • value值被一个同一的虚拟值取代

ConcurrentHashMap线程安全的具体实现方式/底层具体实现

JDK1.7(上面有示意图)

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

JDK1.8 (上面有示意图)

ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

Comparable 和 Comparator的区别

  • Comparable接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序

  • Comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序

  • 借用集合比如Set HashMap,需要自定义排序,需要在实现Comparable接口,重写compareTo()方法

  • 对象数组排序->Collections.sort(),采用Comparator

Comparator定制排序
// 定制排序的用法
Collections.sort(arrayList, new Comparator<Integer>() {

	@Override
	public int compare(Integer o1, Integer o2) {
		return o2.compareTo(o1);
  	}
});
重写compareTo方法实现按年龄来排序
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
public  class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

    /**
     * TODO重写compareTo方法实现按年龄来排序
     */
    @Override
    public int compareTo(Person o) {
        // TODO Auto-generated method stub
        if (this.age > o.getAge()) {
            return 1;
        } else if (this.age < o.getAge()) {
            return -1;
        }
        return age;
    }
}HashMap 容量问题
HashMap 容量为2次幂,是为了数据的的均匀分布,减少hash冲突

毕竟hash冲突越大,代表数组中一个链的长度越大,这样的话会降低hashmap的性能。

根据key的hash确定其在数组的位置时,如果n为2的幂次方,可以保证数据的均匀插入,如果n不是2的幂次方,可能数组的一些位置永远不会插入数据,浪费数组的空间,加大hash冲突

通过 % 求余确定位置性能不如 & 运算。当n是2的幂次方时:hash & (length - 1) == hash % length

这也是扩容2倍的原因,根据hash & oldCap = 1 / 0 确定新的桶位
hash & oldCap(初始是16) > 0:第5位是1,被32取模是存在的,于是新桶值为index(扩容前桶值) + oldCap
hash & oldCap(初始是16) == 0:第5位是1,取模结果和oldCap相同,桶值不变
Hashmap里的hash方法里用到了扰动函数,目的是为了减少hash冲突。
低高位异或

思路是保留高位和低位特征

Jdk7中的源码

h ^=(h >>> 20)^(h >>> 12); return h ^(h >>> 7)^(h >>> 4);

这两行代码里用了四次**>>>运算,因为>>>就是把低位去掉保留高位。然后高位和低位进行^位运算。这样不管是高位发生变化,还是低位发生变化都会造成其结果的中低位**发生变化。

为什么我们关注其结果的中低位呢,那是因为后面算index的时候,用了h & (length-1),它的意思就是把高位去掉。 如果没有进行扰动计算,当key仅仅发生高位变动的时候就会发生hash冲突,这对Hashmap来说往往是致命的

Jdk8中对扰动计算做了简化,但其思路还是一样的,可能还有个原因,jdk8引入和红黑树,就算hash冲突稍微多点性能也还不错。

(key == null)? 0 :(h = key.hashCode())^(h >>> 16);

HashMap中的loadFactor & threshold
loadFactor加载因子

loadFactor加载因子反映数组(HashMap)存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。

loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

threshold

threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。

说一说 ArrayList 的扩容机制吧
详见笔主的这篇文章:通过源码一步一步分析ArrayList 扩容机制

这里以无参构造函数创建的 ArrayList 为例分析

1. 先来看 add 方法
    /**
     * 将指定的元素追加到此列表的末尾。 
     */
    public boolean add(E e) {
   //添加元素之前,先调用ensureCapacityInternal方法
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //这里看到ArrayList添加元素的实质就相当于为数组赋值
        elementData[size++] = e;
        return true;
    }
2. 再来看看 ensureCapacityInternal() 方法
可以看到 add 方法 首先调用了ensureCapacityInternal(size + 1)

之前不太理解的一段代码:minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

当前对象数组是空,取默认容量/当前所需容量的最大值
   //得到最小扩容量
    private void ensureCapacityInternal(int minCapacity) {
    	//当前对象数组是空,取默认容量/当前所需容量的最大值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 获取默认的容量和传入参数的较大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为103. ensureExplicitCapacity() 方法
如果调用 ensureCapacityInternal() 方法就一定会进过(执行)这个方法,下面我们来研究一下这个方法的源码!

  //判断是否需要扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }
HashSet如何检查重复
先进行hashcode的比较,hashcode不同,没有重复出现
hashcode相同,再比较equals(equals true是判定对象相等的唯一条件)
HashSet实现
基本调用HashMap接口

//Dummy value to associate with an Object in the backing Map
//translate 与备份映射中的对象关联的虚拟值
private static final Object PRESENT = new Object();
value值被一个同一的虚拟值取代

ConcurrentHashMap线程安全的具体实现方式/底层具体实现
JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

JDK1.8 (上面有示意图)
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N))synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

Comparable 和 Comparator的区别
Comparable接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
Comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序
借用集合比如Set HashMap,需要自定义排序,需要在实现Comparable接口,重写compareTo()方法
对象数组排序->Collections.sort(),采用Comparator
Comparator定制排序
// 定制排序的用法
Collections.sort(arrayList, new Comparator<Integer>() {

	@Override
	public int compare(Integer o1, Integer o2) {
		return o2.compareTo(o1);
  	}
});
重写compareTo方法实现按年龄来排序
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
public  class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

    /**
     * TODO重写compareTo方法实现按年龄来排序
     */
    @Override
    public int compareTo(Person o) {
        // TODO Auto-generated method stub
        if (this.age > o.getAge()) {
            return 1;
        } else if (this.age < o.getAge()) {
            return -1;
        }
        return age;
    }
}
   private String name;
    private int age;

    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

    /**
     * TODO重写compareTo方法实现按年龄来排序
     */
    @Override
    public int compareTo(Person o) {
        // TODO Auto-generated method stub
        if (this.age > o.getAge()) {
            return 1;
        } else if (this.age < o.getAge()) {
            return -1;
        }
        return age;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值