数组、集合、查找表
什么是数据结构
程序=数据结构+算法
数据结构是计算机存储、组织数据的方式。
数组
数组对象在堆中存储,数组变量属于引用类型,存储数组对象的地址信息,指向数组对象。数组的元素可以看成数组对象的成员变量,只不过是类型相同。
概念:数组是一个简单的数据结构;数组是一种容器,可以存放多个数据值。
特点:1.数组是一种引用数据类型。
2.数组当中的多个数据类型必须统一。
3.数组的长度在程序运行期间不可以修改。数组扩容、缩容都是new了一个新的数组。
4.数组元素按线性顺序排列
数组的初始化
int[ ] array = new int[300] //声明的同时指定数组长度
int[ ] array = new[ ] {1,2,3,4,5} 或 int[] array = {1,2,3,4,5} //声明同时初始化数组
动态初始化数组的时候,若不赋初值,里面的值默认为0,0.0,‘u\0000',false,null(整型,浮点型,字符型,布尔,引用类型)。
静态初始化也有自动赋值,只不过是系统又将大括号里的内容,又进行了赋值。
直接打印数组的名字,得到的是数组首地址的哈希值。
数组常用API
int length() //获取数组长度
static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length) //用于数组的复制。System类中的本地方法,参数分别为原数组、原数组起始下标、目标数组、目标数组起始下标、要复制数组元素的个数
static <T> T[ ] copyOf(T[ ] original, int newLength) //Arrays类中的静态方法,可用于数组的扩容。参数为原数组、新的数组长度。数组一旦创建,长度就不能改变;这里是创建一个新的数组,将原有数组数据复制进去。
static String toString(<T>[ ] a) //Arrays类中静态方法,用于将数组中的内容转化为字符串的形式。参数为基本类型数组。
static <T> Lis t<T> asList(T... a) //Arrays类的静态方法,参数为数组。将数组转换且只能转换为一个List集合,原因是Set集合不允许元素有重复。转化的集合就是表示的该数组。所以对转化集合的操作会直接影响到数组元素。转换而来的集合不能进行add和remove操作,因为数组是不能改变的,是定长的。若想要增删集合元素,需要自行创建一个集合并用addAll方法导入新的集合,或用构造器导入。
引用数组
第一种初始化方法
Student[ ] stu = new Student[3]; //Student 是一个类,包含三个元素,每个元素的都是Studen类型。默认值为null
stu[0] = new Student("aa",44,"芜湖"); //给引用数组赋初值
第二种初始化方法
Student[ ] stu1 = new Student[ ] {new Student("zs",11,"芜湖"),new Student("sd",45,"芜湖")};
数组类型的数组
并不是真正意义上的二维数组。因为二维数组必须是矩阵形式,而java允许每一行元素个数可以不同,只有每行元素个数相同的时候才是二维数组。
int[ ] [ ] arr = new int[ 3] [ ] //相当于声明三个数组元素,这三个数组元素里面的数组元素个数不确定。
arr[0] = new int[5]; //声明每行有多少个数组元素。
arr[1] = new int[20];
arr[2] = new int[100];
arr[0][0] = 5; ...... arr[0][4] = 10; //给每个元素赋值
arr[1][0] = 2; ...... arr[1][19] = 24;
arr[2][0] = 5; ...... arr[2][99] = 28;
集合
java中直接使用数组的情况不多。因为用起来操作很麻烦,添加、删除元素都比较费劲。所以一般我们使用集合来操作多个元素。集合提供了非常多的方法,操作元素起来非常方便。
集合与数组异同
1.集合存的也是元素的引用即地址。
2.数组需要创建时就指定长度预先分配存储空间。集合无需指定长度,动态分配内存,用多少就分配多少内存。
3.集合输出名字会将集合中的元素全部输出出来。而数组输出的是地址
4.集合不一定都是有序的,即存入元素的顺序和元素实际在集合中的顺序可能不同。(如Hashset)
在实际开发中,需要将使用的对象存储于特定的数据结构容器中,JDK提供了这样的容器---集合(Collection)
Collection是一个接口,定义了集合相关操作方法。
Collection接口
java.util.Collection 集合框架。
Collection接口是所有集合的顶级接口,规定了所有集合应当具备的功能。
Collection有两个子接口:List和Set 。List是允许元素重复的有序集,Set不允许元素重复。元素是否重复,取决于元素之间equals()比较的结果。
Collection常用API
boolean add(E e) //向集合中添加给定的元素。E相当于什么都可以向里面存放。
boolean addAll(Collection<? extends E> c) // 并集,将当前集合和给定的集合合并。参数类型为另一个集合引用变量,集合类型可以不一样。
int size() //返回当前集合元素的个数。与数组length不同的是length返回的是数组的总长度,无论其内部是否有元素。size返回的集合中一定有元素
boolean isEmpty() //判断集合中是否是空集。而不是判断集合是不是null,null代表没有这个集合。
void clear() //清空所有集合中的元素。
boolean contains(Object obj) //判断集合中是否包含给定的元素obj,obj本身不再集合中,只是让obj与集合中现有的元素用equals方法进行比较。
boolean containsAll(Collection<?> c) //判断当前集合是否包含给定集合中的所有元素。判断的方法是用equals进行比较,所以如果给定的元素是自己定义的类时,要重写equals方法。
boolean remove(Object obj) //删除集合中给定的元素obj,obj本身不再集合中。同样也是让obj与集合中现有的元素用equals方法进行比较。
boolean removeAll(Collection<?> c) //删除交集。从当前集合中删除给定集合与当前集合相同的元素。
如果集合中有与obj用equals方法比较完之后是true的元素,则删除成功。如果集合中有重复相同的元素,则指删除靠前的哪一个。同样,判断的方法是用equals进行比较,所以如果给定的元素是自己定义的类时,要重写equals方法。
Object[ ] toArray() //可以将当前集合转为数组。返回类型是Object类型,使用时还得造型所以不常用。
Iterator iterator() //该方法可以获取一个用于遍历当前集合元素的迭代器。
Iterator迭代器
java.util.Iterator
该接口是所有迭代器的顶级接口,规定了迭代器遍历集合的统一操作。不同的集合的实现类都实现了一个用于遍历自身元素的迭代器实现类。无需记住它们,只需用接口接收他们,并调用相应的遍历方法即可。
迭代器在遍历集合的过程中,不能通过集合的方法增删元素的,否则会在下次遍历时抛出异常。
迭代器API:
boolean hasNext() //判断迭代器中是否有下一个元素。
E next() //将对应元素取出。E表示返回值可以为任意类型。
default void remove() //删除当前next取出的元素
使用方法:问、取、删 其中删除元素不是必要操作。
1.创建迭代器对象:Iterator it = c.itrator() //c为集合引用变量
2.调用hasNext判断集合是否还有元素可以遍历。
3.调用next取出元素
遍历数组、集合
JDK5时退出了一个新的特性,增强型for循环,也称为新循环。它只是用来遍历集合或数组的。没有循环变量的。
新循环的语法JVM是不认可的,而只是编译器认可。编译器在编译源程序时会将新循环改为传统的循环方式,遍历集合时转换成迭代器循环。
新循环语法
for(String str:array){ }
冒号左边为数组中元素的类型,右边为数组的引用变量。每次循环会依次将数组中的元素赋值给str。
新循环遍历数组在编译时转成普通循环。遍历集合时转换成迭代器循环。用新循环遍历集合时不可以删除元素。
新循环遍历集合
传统迭代器循环:
Iterator it = c.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
新循环:
for(String o:c){
System.out.println("newFor: "+o);
}
集合---List接口和Set接口
以ArrayList和ArraySet为例。
List集合里面的元素是可以重复的,是有序的,可以用下标进行访问元素。它使用起来更加灵活,更像数组。
Set集合里面的元素是不可重复的,是无序的,不可以用下标进行访问元素。
List
List集合的特点:元素可重复,有序。提供了一组操作下标元素的方法
List接口是Collection的子接口,用于定义线性表(即List)数据结构。可以将List理解为存放对象的数组,只不过元素个数可以动态的增加或减少。
List接口两个常见的实现类为ArrayList和LinkedList,分别用动态数组和双向循环链表的方式实现了List接口。
可以认为ArrayList和LinkedList的方法在逻辑上完全一样,而ArrayList更适合与随机访问,LinkedList更适合于插入和删除。若性能要求不是特别苛刻可以忽略这个差别。
构造器以ArrayList为例:
List<E> list = new ArrayList<E>()
List<E> list = new ArrayList<E>(Collection<? extends E> c) //将给定集合中所有元素添加到新创建的集合中去。参数为Collection及其子集的集合。
常用API
<E>get(int index) //获取给定下标对应的元素。参数为下标
<E>set(int index,E elemen) //替换操作,将给定的元素设置到指定位置。返回值为原位置对应的元素(被替换的元素)
List提供了一对重载的add,remove方法。都是支持通过下标操作对应的元素。
void add(int index,E e) //在指定的索引处插入给定的元素。
<E> remove(int index) //删除指定位置对应的元素。返回值为被删除的元素。
List subList(int start,int end) //截取指定范围内的元素。返回值是截取到的子集集合。对子集元素操作就是对源集合对应的操作。
<T> T[ ] toArray(T[ ] a) //重载的toArray。参数为当前集合想要转化为啥类型的数组,数组长度一般指定为集合长度。
若超过,则超过部分为null;若小于自动new一个长度为集合长度的数组。返回值为转化的数组
集合---Collections
java.util.Collections
集合中的一个工具类。提供了很多便于我们操作集合的静态方法。
集合的排序
static <T extends Comparable<? super T>> void sort(List<T> list)
用于对List集合进行自然排序,set集合中HashSet是无序的所以不能排序Set。字符串是按照Unicode编码集排序的。参数为实现了comparable接口的集合引用。 comparable接口定义了比较规则,java提供的常用数据类型,如包装类、字符串类都实现了这个接口,并定义了各自的规则。即若集合中元素为以上类型都不用去重写比较规则。 若集合中的元素是我们自定义的对象,若想使用sort方法,则必须实现comparable接口并重写其中的比较方法。但是这样做是不推荐的。因为这样做具有侵入性。
注:侵入性是指当我们使用某个功能方法时,除了调用该功能方法的语句之外,该功能方法要求我们为其额外的添加其他的代码。例如自定义的类若实现comparable接口并重写方法,那么这时该功能就对我们的程序具有侵入性。
static <T> void sort(List<T> list, Comparator<? super T> c) //第二个参数为比较器,在其实现了comparable接口基础之上自定义排序规则。
这时就需要用到sort的这个个重载方的法,额外的传入一个比较规则(即comparator比较器)来对已经实现了comparable接口并定义了比较规则的元素按照我们定义的比较规则重新排序。
由于可以传入一个额外的比较规则,所以这个sort方法不强制要求集合的元素必须实现Comparable接口。
1 Comparator<String> com = new Comparator<String>(){//使用匿名内部类是因为这个类的实例我们就用一次。 2 public int compare = (String o1,String o2){//重写具有泛型的compare方法。根据定义返回值>0 <0 ==0 三种情况来判断大小。 3 return o1.length()-o2.length(); 4 } 5 }; 6 Collections.sort(list,com);
1 //但是上面的写法还是会有侵入性,的那相对来说减少了。若想没有侵入性,则在重载sort方法中第二个参数直接放入实现代码: 2 Collections.sort(list,new Comparator<T>(){ 3 public int compare = (<T> o1,<T>o2){} 4 });
线程安全的集合
常见的集合如:ArrayList、LinkedList、HashSet 这三个都不是线程安全的,在并发操作时会存在安全问题。这时Collections工具类提供了转换成线程安全的方法。
static <T> List<T> synchronizedList(List<T> list) //重新new一个线程安全的List集合,将给定的List集合传给它。返回值为带有线程安全的集合
static <T> Set<T> synchronizedSet(Set<T> s) //重新new一个线程安全的Set集合,将给定的Set集合传给它。返回值为带有线程安全的集合
查找表 Map
java.util.Map是一个接口,规定了Map操作的相关方法。Map查找表是java提供的一个数据结构。Map查找表结构看起来像是多行两列的表格。左列为key,右列为value
现实生活中,我们经常需要成对存储某些信息。比如,我们使用的微信,一个手机号只能对应一个微信账户。这就是一种成对存储的关系。
Map就是用来存储“键(key)-值(value) 对”的。 Map类中存储的“键值对”通过键来标识,所以“键对象”不能重复。
总是根据key来获取相应的value,对此我们经常将“查询条件”作为key,将要查询的数据作为value进行保存。
Map 接口的常用的实现类 HashMap又称散列表、哈希表。
使用散列算法实现的HashMap,是当今世界上查询速度最快的数据结构,查询速度最快,查询速度最快。
构造器
Map<key的类型,value的类型 > map = new HashMap<key的类型,value的类型>( );
常见API
V put(K key, V value) //向散列表中添加一组key value。key如果在Map中存在则会替换掉原本的value。返回值为被替换的value,若key不存在则返回值为null。
若用基本类型接收put方法的返回值(key在Map中没出现过,value为基本类型)则会出现空指针异常,原因是value类型当前是包装类,在用基本类型接受时会自动拆箱操作,而此时put返回值是null,所以用空引用调用其方法则会空指针异常。解决方法为用包装类接受,返回值为null。
V get(Object key) //获取key值所对应的value。返回值为value,且和put一样不要用基本类型去接收返回值。
int size() //获取Map中元素的个数,每组键值对为一个元素。返回值为int
V remove(Object key) //删除给定的key值所对应的键值对。返回值为删除的value值。
boolean containsKey(Object k) //判断是否包含给定的key。判断方法同样是用equals来判断,如果给定的key、value的类型为自定义的类型则需要重写equals。
boolean containsValue(Object v) //判断是否包含给定的value。
Set <K> keySet( ) //将当前Map中所有的key以一个Set集合形式返回。
Set <Entry<key类型,value类型>> entrySet //将当前Map中所有的key:value以一个Set集合形式返回
遍历查找表Map
1.遍历所有key值
Set <K> keySet( )
将当前Map中所有的key以一个Set集合形式返回。遍历这个集合就等同于遍历了所有的key。K代表key值的类型
Set<String> keyset = map.keySet(); for(String str:keyset) { System.out.println(str); }
2.遍历所有的value(相对来说不常用)
Collection<V> values() //将当前Map中所有的value值以Collection方式返回(因为value可以重复)。此方法不常用。
3.遍历每一组键值对
Set <Entry<key类型,value类型>> entrySet
将当前Map中所有的key:value以一个Set集合形式返回。 Entry相当于键值对的实例,存储在集合中每个元素形式为key=value。java.util.Map.Entry;getKey(),getValue()为其内部的两个方法,分别用于获取Map中对应的key与value
Set<Entry<String,Integer>> entryset = map.entrySet(); for(Entry<String,Integer> e: entryset) { System.out.println(e); }
HashMap
HashMap又称散列表、哈希表。使用散列算法实现的Map,当今世界上查询速度最快的数据结构,查询速度最快,查询速度最快。
Map实际上内部保存元素还是使用的数组来保存元素,但是散列表是根据key元素的hashcode值直接计算出其在数组下标的位置,这样就避免了查询过程中遍历数组这样的操作,所以查询效率不受数组数据量的多少而变化。
hashCode方法是Object定义的方法,意味着所有的类都具有该方法。
常见构造器
Map<T,T>map = new HashMap<T,T>( )
Map<T,T>map = new LinkedHashMap<T,T>( ) //可以保证遍历元素的顺序与put时的顺序一致。
HashMap出现链表的情况
一、当两个Key的hashCode值相同时(即HashMap在内部数组保存时的下标相同)
在HashMap的使用中存在一种情况,当两个Key的hashCode值相同时(即HashMap在内部数组保存时的下标相同)但equals比较不同时(HashMap认为他们是不重复的Key)那么就会在HashMap内部的数组对应位置处出现一个链表。
来保存这两组键值对。
由于链表缺点是遍历速度慢,因此在HashMap中出现链表会影响查询性能。
Object提供的两个方法(hashCode、equals)已经妥善解决上面问题,但是有时候我们定义的类会去重写equals方法,API文档中Object对这两个方法的解释明确说明了上述问题。
重写时要遵守以下原则:
1.成对重写。即我们重写一个类的equals时还应当重写hashCode方法。hashCode方法是将对象的存储地址进行映射。若此时刚好需要我们用自定义的对象去充当hashmap的健值使用时,就会出现我们认为的同一对象,却因为hash值不同而导致hashMap中存了两个对象。
/*equals方法和hashCode方法都不重写的情况*/
public class HashMapTest { private Integer a; public HashMapTest(int a) { this.a = a; } public static void main(String[] args) { Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>(); HashMapTest instance = new HashMapTest(1); System.out.println("instance.hashcode:" + instance.hashCode());
map.put(instance, 1); HashMapTest newInstance = new HashMapTest(1); System.out.println("newInstance.hashcode:" + newInstance.hashCode()); Integer value = map.get(newInstance);
if (value != null) { System.out.println(value); } else { System.out.println("value is null"); } } } //运行结果: //instance.hashcode:929338653 //newInstance.hashcode:1259475182 //value is null
1 public class HashMapTest { 2 private Integer a; 3 4 public HashMapTest(int a) { 5 this.a = a; 6 } 7 8 public static void main(String[] args) { 9 Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>(); 10 HashMapTest instance = new HashMapTest(1); 11 HashMapTest newInstance = new HashMapTest(1); 12 map.put(instance, 1); 13 map.put(newInstance, 2); 14 Integer value = map.get(instance); 15 System.out.println("instance value:"+value); 16 Integer value1 = map.get(newInstance); 17 System.out.println("newInstance value:"+value1); 18 19 } 20 21 public boolean equals(Object o) { 22 if(o == this) { 23 return true; 24 } else if(!(o instanceof HashMapTest)) { 25 return false; 26 } else { 27 HashMapTest other = (HashMapTest)o; 28 if(!other.canEqual(this)) { 29 return false; 30 } else { 31 Integer this$data = this.getA(); 32 Integer other$data = other.getA(); 33 if(this$data == null) { 34 if(other$data != null) { 35 return false; 36 } 37 } else if(!this$data.equals(other$data)) { 38 return false; 39 } 40 41 return true; 42 } 43 } 44 } 45 protected boolean canEqual(Object other) { 46 return other instanceof HashMapTest; 47 } 48 49 public void setA(Integer a) { 50 this.a = a; 51 } 52 53 public Integer getA() { 54 return a; 55 } 56 } 57 //运行结果: 58 //instance value:1 59 //newInstance value:2
1 public class HashMapTest { 2 private Integer a; 3 4 public HashMapTest(int a) { 5 this.a = a; 6 } 7 8 public static void main(String[] args) { 9 Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>(); 10 HashMapTest instance = new HashMapTest(1); 11 System.out.println("instance.hashcode:" + instance.hashCode()); 12 HashMapTest newInstance = new HashMapTest(1); 13 System.out.println("newInstance.hashcode:" + newInstance.hashCode()); 14 map.put(instance, 1); 15 map.put(newInstance, 2); 16 Integer value = map.get(instance); 17 System.out.println("instance value:"+value); 18 Integer value1 = map.get(newInstance); 19 System.out.println("newInstance value:"+value1); 20 21 } 22 23 public boolean equals(Object o) { 24 if(o == this) { 25 return true; 26 } else if(!(o instanceof HashMapTest)) { 27 return false; 28 } else { 29 HashMapTest other = (HashMapTest)o; 30 if(!other.canEqual(this)) { 31 return false; 32 } else { 33 Integer this$data = this.getA(); 34 Integer other$data = other.getA(); 35 if(this$data == null) { 36 if(other$data != null) { 37 return false; 38 } 39 } else if(!this$data.equals(other$data)) { 40 return false; 41 } 42 43 return true; 44 } 45 } 46 } 47 protected boolean canEqual(Object other) { 48 return other instanceof HashMapTest; 49 } 50 51 public void setA(Integer a) { 52 this.a = a; 53 } 54 55 public Integer getA() { 56 return a; 57 } 58 59 public int hashCode() { 60 boolean PRIME = true; 61 byte result = 1; 62 Integer $data = this.getA(); 63 int result1 = result * 59 + ($data == null?43:$data.hashCode()); 64 return result1; 65 } 66 } 67 //运行结果: 68 //instance.hashcode:60 69 //newInstance.hashcode:60 70 //instance value:2 71 //newInstance value:2
摘自 https://www.jianshu.com/p/75d9c2c3d0c1
2.一致性。当两个对象的equals比较为true时,hashCode方法返回的数字必须相等。反之不是必须的但是尽量保证当两个对象hashCode值相同时equals比较也为true,否则会在HashMap使用中作为Key存在时形成链表,影响查询性能。
3.稳定性。当一个对象中参与equals方法比较的属性值没有发生变化的前提下多次调用hashCode方法返回的数字应当不变。
二、容积问题会出现链表
如果内部的数组有五个位置但是现在有6个元素,则出现一个位置有多个元素的机率很高。
hash表中的一些概念:
散列筒(bucket )即为HashMap内部中数组的名称。默认创建出来散列筒的大小为16,也可以使用特定的容量
Capacity: hash表里bucket的数量,即散列数组的大小。
Size:大小,当前散列表中存储数据的实际数量
Load factor:加载因子即size/Capacity,默认值为0.75。当向散列表中增加数组时若大于Load factor则会发生数组扩容并且重新散列(rehash) 因为散列算法数组的长度是必须参加运算的。
经大量测算空闲率为0.75是性能和空间相对平衡的结果。在创建散列表的时候最好给定初始容量,防止rehash影响性能。
Map<k,v>sMap = new HashMap<k,v>(initialCapacity) //给定散列数组的大小,参数值为int 。