集合

Collection

  • Java提供了一种可以存放一组数据的数据结果,称之为集合
  • Collection是一个父接口, 其定义了集合的相关功能

List / Set

  • Collection派生类两个字接口,一个List,另一个是Set
  • List是可重复集,该集合允许存放重复元素,所谓的重复并非是同一个元素,而是指equals方法比较为true的元素
  • Set是不可重复集,该集合中不能将相同的元素存入集合两次

集合持有对象的引用

  • 集合中存储的都是引用类型元素,引用类型变量实际上存储的是对象的地址信息,所以实际上集合只存储了元素对象在堆中的地址,并不是将对象本身存入了集合

add方法

  • Collection定义add方法用于向集合中添加新元素
  • boolean add(E e)
public void test() {
		//定义String类型的集合
		Collection<String> c = new ArrayList<String>();
		//向集合中添加新元素
		c.add("Java");
		c.add("C");
		c.add("Python");
		c.add("PHP");
		System.out.println(c);
	}

contains方法

  • boolean contains(Object o)用于判断给定的元素是否被包含在集合中,若包含则返回true,不包含返回false
public void test() {
		Collection<Cell> c = new ArrayList<Cell>();
		c.add(new Cell(1,2));
		c.add(new Cell(2,3));
		c.add(new Cell(3,4));
		c.add(new Cell(4,5));
		c.add(new Cell(5,6));
		
		Cell a = new Cell(4,5);
		//判断给定的元素是否被包含在集合中,若包含则返回true,不包含返回false
		boolean flag = c.contains(a);
		System.out.println(flag);
	}

size / clear / isEmpty

  • int size(); 获取当前集合中元素的总和
  • void clear(); 清空集合
  • boolean isEmpty(); 判断当前集合是否不包含元素
public void test() {
    Collection<String> c = new ArrayList<String>();
    c.add("a");
    c.add("b");
    c.add("c");
    c.add("d");
    int s =c.size();
    System.out.println(s);
    c.clear();
    c.add("e");
    System.out.println(c);
    boolean i = c.isEmpty();
    System.out.println(i);
  }

 

集合框架

Collection

addAll和containall方法

  • boolean addAll(Collection c)用于将给定的集合中所有元素添加到当前集合中
  • boolean conatinAll(Collection c)用于判断当前集合是否包含给定集合中的所有元素
public void test() {
		Collection<String> c1 = new ArrayList<String>();
		c1.add("a");
		c1.add("b");
		c1.add("c");
		Collection<String> c2 = new ArrayList<String>();
		c2.add("a");
		c2.add("b");
		//判断给定的元素是否被包含在集合中,若包含则返回true,不包含返回false
		System.out.println(c1.containsAll(c2));
	}

迭代器

Iterator

Iterator()方法用于返回一个实现了iterator接口的对象。可以使用这个迭代器对象依次访问集合中的元素。

  • Collection提供了一个遍历集合的通用方法,迭代器
  • Iterator iterator() 迭代器是一个接口
public void test5() {
    Collection<String> c = new HashSet<String>();
    c.add("C++");
    c.add("abc");
    c.add("php");
    c.add("Java");
    //Iterator iterator()迭代器是一个接口
    Iterator<String> it = c.iterator();
    //判断集合是否还有元素可以遍历
    while(it.hasNext()) {
      String str = it.next();
      System.out.println(str);
    }
  }

hasNext和next方法

通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾。next方法将抛出一个NoSuchElementExpection。因此,需要在调用next之前调用hasNext方法。如果迭代器对象还有很对个供访问的元素,这个方法就返回true。如果想要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true时返回调用next方法。

  • boolean hasNext(); 判断集合是否含有元素可以遍历
  • E next() 返回迭代的下一个元素
  • 迭代器遵循“先问后取”的方式。当确定hasNext方法的返回值为true时,再通过next方法取元素
public void test6() {
    Collection<String> c = new HashSet<String>();
    c.add("C++");
    c.add("abc");
    c.add("php");
    c.add("Java");
    System.out.println(c);
    //Iterator iterator()迭代器是一个接口
    Iterator<String> it = c.iterator();
    while(it.hasNext()) {
      String str = it.next();
      if(str.indexOf("c") != -1) {  //检索字母c
        it.remove();
      }
    }
    System.out.println(c);
  }

remove方法

Iterator接口的remove方法将会删除上次调用next方法时返回的元素。大多数情况下,在决定删除某个元素之前应该先看一下这个元素是很具有实际意义的。然而,如果想要删除这个位置上的元素,仍然需要越过这个元素。

对next方法和remove方法的调用具有互相依赖性。如果调用remove之前没有调用next将是不合法的。如果这样做,将会抛出一个IllegalStateExpection异常。

  • void remove()用于删除迭代器当次从集合中获取的元素

增强for循环

  • Java5.0之后推出新特性,增强for循环,也称之为新循环。该循环不通用于传统循环的工作,只用于遍历集合或数组
  • 语法:
for (元素类型 e:集合或数组名){
  循环体
}
  • 新循环并非新语法,而是在编译过程中,编译器会将新循环转换为迭代器模式,所以新循环本质上是迭代器
public void test7() {
    Collection<Integer> c = new HashSet<Integer>(); 
    c.add(123);
    c.add(234);
    c.add(345);
    c.add(456);
    c.add(567);
    //增强for循环
    for(Integer a : c) {
      System.out.println(a);
    }
  }

 

泛型机制

泛型在集合中的应用

泛型是Java SE 5.0引入的特性,泛型的本质是参数化类型。在类、接口和方法的定义过程中,所操控的数据类型被传入的参数指定。

  • Java泛型机制广泛应用在集合框架中。所有的集合类型都带有泛型参数,这样在创建集合时可以指定放入集合中的对象类型。Java编译器可以据此进行类型检查。
  • 举个例子:
//比如ArrayList,其定义时是这样的:
public class ArrayList<E> {
    … … …                
    public boolean add(E e) {…};
    public E get(int index) {…};
} 
/*
由此我们可以看出,再声明ArrayList时,类名的右侧有一个<E>。“<>”表示泛型,而其中可以使用数字字母下划线(数字不能时第一个字符)来表示泛型的名字。(通常我们使用一个大写字母来表示)这时,在类中声明的方法的参数,返回值类型可以被定义为泛型。这样创建对象可以将类型最为参数传递,此时,类定义所有的E将被替换成传入的参数。
*/
// 例如:
ArrayList<String> list = new ArrayList<String>();//泛型E在这里被指定为String类型
list.add("One");//那么add方法的参数就被替换为String类型
list.add(100);//这里就会出现编译错误,因为这里的参数应为String类型。

 

集合操作——线性表

List

List接口是Collection的子接口,用于定义线性表数据结构;可以将List理解为存放对象的数组,只不过其元素可以动态的增加或减少,并且List是可重复集

ArrayList / LinkedList

List接口的两个常见的实现类ArrayList和LinkedList,分别用动态数组和链表实现类接口

可以认为ArrayList和LinkedList的方法在逻辑上完全一样。只是在性能上有一定的差别。ArrayList更适用于随机访问而LinkedList更适合于插入和删除;在性能要求不是特别苛刻的情形下可以忽略这个差别。

set与get方法

List除了继承Collection定义的方法外,还根据其线性表的数据结构定义了一系列方法,其中最常用的就是基于下标的get和set方法

  • E get(int index);获取集合中指定下标对应的元素,下标从0开始。
  • E set(int index, E element);将给定的元素存入给定的位置,并将原位置的元素返回。
public void test(){
		List<String> list = new ArrayList<String>();
		list.add("a");
		list.add("b");
		list.add("c");
		list.add("d");
		list.add("e");
    //get方法遍历List
		for(int i=0; i<list.size(); i++) {
			System.out.println(list.get(i));
		}
		String value = list.set(1,"F");
		System.out.println(value);			//b
		System.out.println(list);				//[a, F, c, d, e]
    //交换位置1和3上的元素
    list.set(1, list.set(3, list.get(1)));
		System.out.println(list);				//[a, d, c, F, e]
	}

插入和删除

List根据下标的操作还支持插入与删除操作:

  • Void add(int index,E element);将给定的元素插入到指定位置,原位置及后续元素顺序向后移动
  • E remove (int index);删除给定位置的元素,并将被删除的元素返回。
public void test() {
		List<String> list = new ArrayList<String>();
		list.add("a");
		list.add("b");
		list.add("c");
		//将下标为1的元素替换为指定元素
		list.add(1, "D");
		System.out.println(list);//[a, D, b, c]
		//将下标为2的元素删除
		list.remove(2);
		System.out.println(list);//[a, D, c]
	}

subList方法

List的subList方法用于获取子List。

需要注意的是,subList获取的List与原List占有相同的储存空间,对子List的操作会影响原List。

  • List<E> subList(int formIndex,int toindex); 含头不含尾
public void test() {
		List<Integer> list = new ArrayList<Integer>();
		//向list中添加数据
		for(int i=0; i<10; i++) {
			list.add(i);
		}
		System.out.println(list);//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
		//找出指定范围的元素,含头不含尾
		//subList获取的List和原List占有相同的数据空间
		List<Integer> sublist = list.subList(3, 8);
		//将提取出来的子表每位*10
		for(int i=0; i<sublist.size(); i++) {
			sublist.set(i, sublist.get(i)*10);
		}
		System.out.println(sublist);//[30, 40, 50, 60, 70]
		System.out.println(list);//[0, 1, 2, 30, 40, 50, 60, 70, 8, 9]
	}

List转换为数组

List和toArray方法用于将集合转换为数组。但实际上该方法是在Collection中定义的,所以所有的集合都具备这个功能。

  • 其有两个方法:
  1. Object[] toArray();
  2. T[] toArray(T[] a);

其中第二个是比较常用的,我们可以传入一个指定类型的数组,该数组的元素类型应与集合的元素类型保持一致。返回值则是转换后的数组,该数组会保存集合的所有元素。

public void test4() {
		List<String> list = new ArrayList<String>();
		list.add("a");
		list.add("b");
		list.add("c");
		//将list表通过toArray()方法转换为数组
		//传入的数组不需要指定长度
		String[] arr = list.toArray(new String[]{});
		System.out.println(arr.toString());
		//重写toString
		System.out.println(Arrays.toString(arr));
	}

数组转换为List

Arrays类中提供了一个静态方法asList,使用该方法我们将一个数组转换为对应的List集合。

  • 其定义方法为:
    Static List<T> asList(T...a);

返回的List的集合元素类型由传入的数组的元素类型决定。

返回的集合我们不能对其增删元素,否则会抛出异常。并且对集合的元素进行的修改会影响数组对应的元素。

public void test() {
		String[] arr = {"a","b","c"};
		//将数组arr通过Arrays.asList()方法转换为list表
		List<String> list = Arrays.asList(arr);
		System.out.println(list);
		list.add("d");			//抛出异常:UnsupportedOperationException
		
		//			java.utilArray$ArrayList
		//得到list表的类和名字
		System.out.println(list.getClass().getName());
		List<String> list2 = new ArrayList<String>();
		list2.addAll(list);
		System.out.println(list2);
	}

List排序

Collections.sort方法实现排序

Collections是集合的工具类,它提供了很多便于我们操作集合的方法。

  • 该方法的定义为:
    void sort(List<T> list);其作用是对集合元素进行自然排序(小至大)。
public void test() {
    List<Integer> list = new ArrayList<Integer>();
    //随机生成数
    Random r = new Random();
    for(int i=0; i<10; i++) {
      list.add(r.nextInt(100));//nextInt(100):获得100以内的随机数
    }
    System.out.println(list);	//[76, 98, 24, 13, 44, 16, 50, 57, 20, 21]
    Collections.sort(list);
    //升序排序
    System.out.println(list);	//[13, 16, 20, 21, 24, 44, 50, 57, 76, 98]
  }

Comparable

若想对某个集合的元素集合自然排序,该集合的元素有一个要求,就是这些元素必须是Comparable的子类

  • Comparable是一个接口,用于定义其子类是可比较的,因为该接口有一个抽象方法:
    int comparableTo(T t);

所有子类都需要重写该方法来定义对象间的比较规律,该方法要求返回一个整数,这个整数不关心具体的值,而是关注取值范围。

  1. 当返回值>0时,表示当前对象比参数给定的对象大
  2. 当返回值<0时,表示当前对象比参数给定的对象小
  3. 当返回值=0时,表示当前对象和参数给定的对象相等
public class Cell implements Comparable<Cell>{
	
	int row;
	int col;
	
	public Cell(int row, int col){
		this.row = row;
		this.col = col;
	}
	
	public int compareTo(Cell c) {
		//根据row比较大小
		return this.row - c.row;
		
	}
}

那么Collections的sort在进行排序时就会根据集合中元素的compareTo方法的返回值来判断大小从而进行自然排序。

public void test1() {
		List<Cell> cell = new ArrayList<Cell>();
		cell.add(new Cell(1,2));
		cell.add(new Cell(4,3));
		cell.add(new Cell(3,4));
		Collections.sort(cell);
		System.out.println(cell);
	}

comparator

一旦Java类实现了Comparable,其比较逻辑就已经确定;如果希望在排序的操作中临时指定比较规定,可以采用Comparator接口回调的方式。

  • 该接口要求实现类必须重写其定义的方法:
    int compare(T o1, T o2);

该方法的返回值要求,o1>o2则返回值应>0,若o1<o2则返回值应<0,若o1=o2则返回值应为0

public void test1() {
		List<Cell> cell = new ArrayList<Cell>();
		cell.add(new Cell(1,2));
		cell.add(new Cell(4,3));
		cell.add(new Cell(3,4));
		//Comparator为接口
		//按照col值的大小排序
		Collections.sort(cell, new Comparator<Cell>() {
			@Override
			public int compare(Cell o1, Cell o2) {
				return o1.col-o2.col;}
			});
		System.out.println(cell);
	}

队列和栈

Queue

队列(Queue)时常用的数据结构,可以将队列看成特殊的线性表,队列限制了对线性表的访问方式:只能从线性表的一端添加(offer)元素,另一端取出(poll)元素。

队列遵循“先进先出”原则。

JDK中提供了Queue接口,同时使得LinkedList实现了该接口(选择LinkedList实现Queue的原因在于Queue经常要惊醒插入和删除的操作,而LinkedList在这方面效率较高)。

  • Queue提供了操作队列的相关方法,其主要方法如下:
  1. Boolean oft(E e);将元素添加到队列的末尾,若添加成功返回true
  2. E poll();从队首删除并返回该元素
  3. E peek();返回队首元素,但不删除
public void test() {
		Queue<String> queue = new LinkedList<String>();
		//使用offer() 方法将元素添加到队列中
		queue.offer("a");
		queue.offer("b");
		queue.offer("c");
		System.out.println(queue);
		//返回队列首元素打不删除
		String str = queue.peek();
		System.out.println(str);
		//从队首删除并返回该元素
		String s = queue.poll(); 
		System.out.println(s);
		System.out.println(queue);
	}

Deque

Deque是Queue的子接口,定义了所谓“双端队列”即从队列的两端分别可以入队(offer)和出队(poll),LinkedList实现了该接口。

如果将Deque限制为只能从一端入队和出队,则可以实现“栈(stack)”的数据结构,对于栈而言,入栈称之为push,出栈称之为pop。

栈遵循“先进后出”的原则。

  • Deque提供了操作栈的相关方法,其主要方法如下:
  1. void push(E e);将给定元素“压入”栈汇总,存入的元素会在栈首。即:栈的第一个元素。
  2. E pop();将栈首元素删除并返回。
public void test() {
		Deque<String> stack = new LinkedList<String>();
		//void push(E e)将给定的元素"压入"栈中
		stack.push("a");
		stack.push("b");
		stack.push("c");
		System.out.println(stack);
		System.out.println(stack.peek());
	}

Map

Map接口

Java提供了一组可以以键值对(key-value)的形式存储数据的数据结构,这种结构称之为Map。可以Map看成一个多行两列的表格,其中第一列存储key,第二列存储value,而每一行就相当于一组key-value对,表示一组数据结构。

Map对存入的元素只有一个要求,就是key不能重复,所谓的不能重复指的是Map中不能包含两个equals为true的key。

Map对于key、value的类型没有严格要求,只要是引用类型均可。但是为了保证在使用时不会造成数据混乱,通常我们会使用泛型去约束key与value的类型。

put方法

  • Map提供了一个方法:V put(K k,V v);

该方法的作用是将key-value对存入Map中,因为Map中不允许出现重复的key,所以若当次存入的key已经在Map中存在,则是替换value,而返回值则为被替换的元素。若key不存在,那么返回值就为null。

get方法

  • Map提供了一个方法:V get(Object key);

该方法的作用是根据给定的key去查找Map中对应的value并返回,若当前Map汇总不包含给定的key,那么返回值为null。

containKey方法

  • 其方法定义如下:boolean containsKey(Object key);

若当前Map中包含给定的key(这里检查是否包含是根据key的equals比较结果为依据的)则返回true

public void test() {
		Map<String,Integer> map = new LinkedHashMap<String,Integer>();
		//使用put方法将一组键值对存入map中
		map.put("城市一", 232);
		map.put("城市二", 346);
		map.put("城市三", 421);
		map.put("城市四", 295);
		//获取给定的key获取value并返回
		Integer str1 = map.get("城市二");
		System.out.println(str1);
		//判断给定的key是否已经存在
		boolean str2 = map.containsKey("城市二");
		System.out.println(str2);
		System.out.println(map);
		//封装为entry实例
		Set<Map.Entry<String,Integer>> entry = map.entrySet();
		//增强for循环
		for(Map.Entry<String, Integer> e: entry) {
			System.out.println(e.getKey()+":"+e.getValue());
		}
	}

HashMap

hash表原理

HashMap是Map的一个常用的子类实现,其实使用散列算法实现的。

HashMap内部维护着一个散列数组(就是一个存放元素的数组)称之为散列桶,而当我们向HashMap中存入一组键值对时,HashMap首先获取key这个对象的hashcode()方法的返回值,然后使用该值进行散列算法,得出一个数字,这个数字就是这组键值对要存入散列数组的中的下标位置。

得到了这个下标位置后,HashMap还会查看散列数组当前位置是否包含该元素。(这里要注意的是,散列数组中每个元素并非是直接存储键值对的,而是存入了一个链表,这个链表中的每个节点才是真实保存这组键值对的)检查是否包含该元素时根据当前要存入的key在当前位散列数组对应位置中的链表里是否已经包含这个key,若不包含则将这组键值对存入链表,否则就替换value。

那么在获取元素时,HashMap同样先根据hashcode值进行散列算法,找到它在散列数组中的位置,然后进行遍历该位置的链表,找到该key所对应的value之后返回。

链表中只能存入一个元素,所以实际上,HashMap在放入一组键值对之前,会对这组键值对封装为一个Entry的实例,然后将该实例存入链表。

hashcode()方法

HashMap的存取是依赖于key的hashcode方法的返回值的,而hashcode方法实际上是在Object中定义的。

  • 其定义如下:int hashCode();
  • 重写一个类的hashcode()方法有以下注意事项:
  1. 若一个类重写了equals方法,那就应当重写hashcode()方法。
  2. 若两个对象equals方法比较为true,那么他们就应当具有相同的hashcode值。
  3. 对于同一个对象而言,在内容没有发生改变的情况下,多次调用hashcode()方法应当总是返回相同的值。
  4. 对于两个对象equals比较为false的,并不要求hashcode值一定不同,但是应尽量保证不同,这样可以提高散列表性能。

装载因子及HashMap优化

  • 在散列表中有以下名次需要了解:
  1. Capacity:容量。hash表里bucket(桶)的数量,也就是散列表的大小
  2. Initial capacity:初始容量。创建hash表的初始bucket的容量,默认构建容量时16,也可以使用特定容量。
  3. Size:大小。当前散列表中存储数据的容量
  4. Load factor:加载因子。默认值0.75(就是75%),向散列表增加数据时如果size/capacity的值大于Local factory则发生扩容并且重新散列(rehash)。

那么加载因子较小时散列查找性能会提高,同时也浪费了散列桶空间容量,0.75的性能和空间相对平衡的结果,在创建散列表时指定合理容量,从而可以减少rehash提高性能。

有序Map

Map接口的哈希表和链表实现,具有可预知的迭代顺序。此实现与HashMap的不同在于LinkedHashMap维护着一个双向循环链表。此链表迭代顺序通常就是存放元素的顺序。

如果Map中重新存入已有的key,那么key的位置不会发生改变,只是将value值替换。

 


❤️ END ❤️