数组与集合
数组与集合
数组与集合区别
-
数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),而JAVA集合可以存储和操作数目不固定的一组数据。 所有的JAVA集合都位于 java.util包中, JAVA集合只能存放引用类型的的数据,不能存放基本数据类型。
-
数组与集合的区别(参考文章:《Thinking In Algorithm》03.数据结构之数组):
- 有人想有可以自动扩展的数组,所以有了List ;有的人想有没有重复的数组,所以有了set ;有人想有自动排序的组数,所以有了TreeSet,TreeList,Tree** -》而几乎有有的集合都是基于数组来实现的. 因为集合是对数组做的封装,所以,数组永远比任何一个集合要快
- 但任何一个集合,比数组提供的功能要多:元素类型不声明;动态大小;数字没法只读,集合提供了ReadOnly方法可以返回只读
java中的数组(即Array)
java所有“存储及随机访问一连串对象”的做法,array是最有效率的一种.
- 效率高,但容量固定且无法动态改变。
array还有一个缺点是,无法判断其中实际存有多少元素,length只是告诉我们array的容量
例如:声明数组
String [] arr;
int arr1[];
String[] array=new String[5];
int score[]=new int[3];
也可以直接赋值:
int[] m = { 1, 2, 3 };
String[] strings = { "aaa", "bbb" };
- Java中有一个Arrays类,专门用来操作数组array。
方法 作用 equals() 比较两个array是否相等。array拥有相同元素个数,且所有对应元素两两相等。 fill() 将值填入array中。 sort() 用来对array进行排序。 binarySearch() 在排好序的array中寻找元素。 System.arraycopy() array的复制。
示例
int[] arr3=new int[5];
Arrays.fill(arr3, 10); //将数组全部填充10
//遍历输出
for (int i = 0; i < arr3.length; i++) {
System.out.println(arr3[i]);
}
int[] arr4 = {3, 7, 2, 1, 9};
Arrays.sort(arr4); //.sort(int[] a) 放入数组名字排序
for (int i = 0; i < arr4.length; i++) {
System.out.println(arr4[i]);
}
java中的集合
若撰写程序时不知道究竟需要多少对象,需要在空间不足时自动扩增容量,则需要使用容器类库,array不适用。所以就要用到集合
-
集合分类:Collection:List、Set || Map:HashMap、HashTable
-
集合 List Set Map的区别
Java中的集合包括三大类,它们是Set、List和Map,它们都处于java.util包中,Set、List和Map都是接口,它们有各自的实现类。Set的实现类主要有HashSet和TreeSet,List的实现类主要有ArrayList,Map的实现类主要有HashMap和TreeMap。- List中的对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,如通过list.get(i)方式来获得List集合中的元素
- Set中的对象不按特定方式排序,并且没有重复对象。但它的有些实现类能对集合中的对象按特定方式排序,例如TreeSet类,它可以按照默认排序,也可以通过实现java.util.Comparator来自定义排序方式。
- Map中的每一个元素包含一个键对象和值对象,它们成对出现。键对象不能重复,值对象可以重复。
-
总结
-
Collection 和 Map 的区别:容器内每个为之所存储的元素个数不同。Collection类型者,每个位置只有一个元素。Map类型者,持有 key-value pair,像个小型数据库
-
各自旗下的子类关系:
-
Collection
- List:将以特定次序存储元素。所以取出来的顺序可能和放入顺序不同
ArrayList / LinkedList / Vector,Stack继承自Vector(其中Vector和Stack是线程安全的) - Set : 不能含有重复的元素
HashSet / TreeSet - Collection集合也有一个专门的操作类Collections
- List:将以特定次序存储元素。所以取出来的顺序可能和放入顺序不同
-
Map(Hashtable 既不允许 null 键也不允许 null 值但 HashMap 允许任意数量的 null 值和最多一个 null 键。另外HashTable是线程安全的)
- HashMap
- HashTable
- TreeMap
-
-
-
集合的范例及使用
定义student对象,主要属性如下:
public class Student {
// 成员变量
private String name;
private int age;
private int id;
public Student(String name, int age, int id) {
this.name = name;
this.age = age;
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
集合使用
public class Step1TestMain {
public static void main(String[] args) {
// 创建5个学生对象,并赋值。
Student s1 = new Student("小明", 27);
Student s2 = new Student("小红", 30);
Student s3 = new Student("小强", 30);
Student s4 = new Student("旺财", 12);
Student s5 = new Student("张三", 35);
//List测试,主要是ArrayList
List<Student> list = new ArrayList<Student>();
list.add(s1);
list.add(s2);
list.add(s3);
list.add(s4);
list.add(s5);
//s1再次添加,list是不判断重复的
list.add(s1);
//List两种遍历方式,输出一致
for (Student student : list) {
System.out.println(student.toString());
}
//遍历方式2
for (int i=0; i< list.size(); i++) {
System.out.println(list.get(i).toString());
}
//Set测试,主要是HashSet
Set<Student> set = new HashSet<Student>();
set.add(s1);
set.add(s2);
set.add(s3);
set.add(s4);
set.add(s5);
set.add(s1);
System.out.println("set不包含重复:"+ set.size());
//HashMap测试,每次输出结果都不一致,内部是无序的
Map<String, Student> map = new HashMap<String, Student>();
map.put(s1.getName(), s1);
map.put(s2.getName(), s2);
map.put(s3.getName(), s3);
for (Entry<String, Student> entry : map.entrySet()) {
System.out.println(entry.getValue().toString());
}
}
}
集合类的详细说明
List中删除元素
【答案解析】
推荐采用iterator进行遍历,发现符合条件的用iterator.remove进行删除,如下:
Iterator<String> it = list.iterator();
while(it.hasNext()){
String x = it.next();
if(x.equals("del")){
it.remove();
}
}
这种方式可以正常的循环及删除。但要注意的是,使用iterator的remove方法,如果用list的remove方法同样会报上面提到的ConcurrentModificationException错误。
如果只删除一个元素的话也可以for循环然后找到要删除的元素,调用list.remove(obj)的方式进行删除,如下,一般建议只用iterator的方式:
//方式1:for循环遍历list
for(int i=0;i<list.size();i++){
if(list.get(i).equals("del"))
list.remove(i);
}
//这种方式的问题在于,删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素。比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用。
//方式2:增强for循环
for(String x:list){
if(x.equals("del"))
list.remove(x);
}
//这种方式的问题在于,删除元素后继续循环会报错误信息ConcurrentModificationException,因为元素在使用的时候发生了并发的修改,导致异常抛出。但是删除完毕马上使用break跳出,则不会触发报错。
//方式1和方式2只是用来扩充思路,实际一般不采用
HashMap遍历
结合安全性和效率一般使用迭代器 EntrySet方法(KeySet需要再次遍历HashMap,效率低)
HashMap<Integer, String> map=new HashMap<>();
map.put(1,"赵");
map.put(2,"钱");
map.put(3,"孙");
map.put(4,"李");
// 迭代器 EntrySet
Iterator<Map.Entry<Integer, String>>iterator=map.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<Integer, String> entry=iterator.next();
System.out.println(entry.getKey()+":"+entry.getValue());
}
// 迭代器 KeySet
Iterator<Integer> iterator2=map.keySet().iterator();
while(iterator.hasNext()){
Integer key=iterator2.next();
System.out.println(key+":"+map.get(key));
}
// ForEach EntrySet
for(Map.Entry<Integer, String> entry:map.entrySet()){
System.out.println(entry.getKey()+":"+entry.getValue());
}
// ForEach KeySet
for(Integer key:map.keySet()){
System.out.println(key+":"+map.get(key));
}
// lambda
map.forEach(
(key,value)->{
System.out.println(key+":"+value);
});
// stream
map.entrySet().stream()
.forEach(
(entry)->{
System.out.println(entry.getKey()+":"+entry.getValue());
});
Collection和Collections区别
【参考答案】
java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
- Collection
- List
- LinkedList
- ArrayList
- Vector
- Stack
- Set
- List
java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
List排序
利用Collections类的 java.util.Collections.sort(java.util.List, java.util.Comparator) 方法,自定义比较器对象对指定对象进行排序
ArrayList list=new ArrayList();
list.add(new Student(1,"石一歌",22));
list.add(new Student(2,"茉小彪",23));
list.add(new Student(3,"小林",24));
Collections.sort(
list,
(Comparator<Student>)(s1,s2)->{
// 排序规则:按照汉字拼音首字母排序
Comparator<Object> com=Collator.getInstance(Locale.CHINA);
return com.compare(s1.getName(),s2.getName());
});
list.forEach(x->System.out.println(x.toString()));
通过实现Comparable接口重写compareTo方法来实现list的排序
public class Student implements Comparable<Student> {
private Integer id;
private String name;
private Integer age;
.....
@Override
public int compareTo(Student o) {
Comparator<Object> com = Collator.getInstance(Locale.CHINA);
return com.compare(this.getName(), o.getName());
}
}
ArrayList list=new ArrayList();
list.add(new Student(1,"石一歌",22));
list.add(new Student(2,"茉小彪",23));
list.add(new Student(3,"小林",24));
Collections.sort(list);
list.forEach(x->System.out.println(x.toString()));
集合的内部源码
HashMap实现原理
-
关键点:
-
使用“链地址法”解决哈希冲突,内部使用数组(Entry数组)实现,每个位置是包含一个key-value键值对的Entry基本组成单元,放入元素的过程主要有两步
-
put
- HashMap 通过 key 的 hashCode 经过 hash() 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置
- 如果当前位置无元素的话直接放在当前位置
- 如果当前位置有元素的话,通过key的equals方法进行判断,如果返回true的话直接更新当前位置,如果false的话需遍历链表,存在即覆盖,否则新增,此时链表时间复杂度为O(n)
- HashMap 通过 key 的 hashCode 经过 hash() 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置
-
get
- HashMap 通过 key 的 hashCode 经过 hash() 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置
- 如果该位置无链表的话直接返回
- 如果该位置有链表的话需遍历链表,然后通过key对象的equals方法逐一比对查找
- HashMap 通过 key 的 hashCode 经过 hash() 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置
-
-
HashMap 的长度为什么是 2 的幂次方
在指定容量的情况下,容量为不小于指定容量的2的幂数,元素超过阈值会扩容到原来的两倍
默认容量16,阈值0.75
- 数组计算下标方法 key 的 hashCode 经过 hash() 处理过后得到的值对数组长度取余,即 hash%length
- 在length=2^n 的前提下,hash%length可转化为hash&(length-1),以此来提高效率和减少冲突
-
100个元素不扩容,初始设置元素为多少
- 100/0.75=133,128<133<256
- 设置元素范围[129,256]
-
HashMap整体结果如下图:
-
【备注】jdk1.8起,如果链表的key数量不超过8仍然使用链表,如果超过了8个将链表转换为红黑树,用来解决链表比较长的时候查找性能下降的问题。
HashSet实现原理
HashSet是一个没有重复元素的集合,HashSet其实是由HashMap实现的,HashMap中保存的是键值对,然而我们只能向HashSet中添加key,原因在于HashSet的Value其实都是同一个对象,这是HashSet添加元素的方法,可以看到辅助实现HashSet的map中的value其实都是Object类的同一个对象。
如下:
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
TreeMap实现原理
内部使用红黑树来进行排序的,每个元素是Entry的结构,如下:
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;
}
为了维护数据的唯一性。在存入数据的时候,如果在构造方法中传递了Comparator对象,那么就会以Comparator对象的方法进行比较。否则,则使用Comparable的 compareTo(T o)
方法来比较。
具体的实现是:调用比较方法,返回-1 的时候,添加到左子树,返回1 的时候 添加到 右子树。返回0 有相同数据 不添加该元素!
创建时传入迭代器代码如下:
// 比较器
private final Comparator<? super K> comparator;
// 红黑树根节点
private transient Entry<K,V> root = null;
//默认构造方法,comparator为空,代表使用key的自然顺序来维持TreeMap的顺序,这里要求key必须实现Comparable接口
public TreeMap() {
comparator = null;
}
//用指定的比较器构造一个TreeMap
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
// 构造一个指定map的TreeMap,同样比较器comparator为空,使用key的自然顺序排序
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
添加元素的时候,如果comparator不为空会调用compare方法;否则会调用key的compareTo方法,这要求key必须实现Comparable接口
代码如下:
//传入的比较迭代器
Comparator<? super K> cpr = comparator;
if (cpr != null) {
...
cmp = cpr.compare(key, t.key);
...
} else {
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
这段代码的意思是:
1. K implements Comparable is ok
2. That if any super class of K has implemented Comparable is OK too
...
cmp = k.compareTo(t.key);
...
}
TreeSet实现原理
TreeSet也是没有重复的排序集合,TreeSet也是由TreeMap实现的,这个跟HashSet的实现方式是类似的。
ConCurrentHashMap实现原理:
ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁
ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。
更令人惊讶的是ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。【了解】:在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
ArrayList实现原理
ArrayList基于数组实现,
- ArrayList 实际上是通过一个数组去保存数据的。当我们构造ArrayList时;若使用默认构造函数,则ArrayList的默认容量大小是10。
- 当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=“(原始容量x3)/2 + 1”。
- ArrayList的克隆函数,即是将全部元素克隆到一个数组中。
- ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
LinkedList实现原理
LinkedList基于双向链表实现
- LinkedList 实际上是通过双向链表去实现的。
它包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。 - 从LinkedList的实现方式中可以发现,它不存在LinkedList容量不足的问题。
- LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。
Vector实现原理
Vector是ArrayList的线程安全版本,vector是线程(Thread)同步(Synchronized)的,此种实现方式(即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性)需要很高的花费,因此,访问它比访问ArrayList慢
Stack实现原理
Stack是java实现的栈,Stack继承于Vector,意味着Vector拥有的属性和功能,Stack都拥有, Stack也是线程安全的。
equals()相等的两个对象,hashcode是否相同,为什么
【参考答案】:
不对,理解完集合类的内部实现后对这个将非常明确。
如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。Java对于eqauls方法和hashCode方法是这样规定的:
- 如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;
- 如果两个对象的hashCode相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。