以前学习了java中从语法到经常使用类的部分。在编程中有这样一类需求,就是要保存批量的相同数据类型。针对这种需求通常都是使用容器来存储。以前说过Java中的数组,可是数组不能改变长度。Java中提供了另外一种存储方式,就是用容器类来处理这种须要动态添加或者删除元素的状况
java
Java中最多见的容器有一维和多维。单维容器主要是一个节点上存储一个数据。好比列表和Set。而多维是一个节点有多个数据,例如Map,每一个节点上有键和值。
单维容器的上层接口是Collection,它根据存储的元素是否为线性又分为两大类 List与Set。它们根据实现不一样,List又分为ArrayList和LinkedList;Set下面主要的实现类有TreeSet、HashSet。
它们的结构大体以下图:
算法
Collection 是单列容器的最上层的抽象接口,它里面定义了全部单列容器都共有的一些方法:编程
boolean add(E e)
:向容器中添加元素void clear()
: 清空容器boolean contains(Object o)
: 判断容器中是否存在对应元素boolean isEmpty()
: 容器是否为空boolean remove(Object o)
: 移除指定元素<T> T[] toArray(T[] a)
: 转化为指定类型的数组list是Collection 中的一个有序容器,它里面存储的元素都是按照必定顺序排序的,可使用索引进行遍历。容许元素重复出现,它的实现中有 ArrayList和 LinkedList数组
Set集合是Collection下的另外一个抽象结构,Set相似于数学概念上的集合,不关心元素的顺序,不能存储重复元素。ide
从上面的描述看,想要在HashSet中添加元素,须要首先计算hash值,在判断集合中是否存在元素。这样在存储自定义类型的元素的时候,须要保证类可以正确计算hash值以及进行类型的相等性判断。所以要重写类的hashCode
和equals
方法。
例以下面的例子函数
class Person{ private String name; private int age; Person(){ } Person(String name, int age){ this.name = name; this.age = age; } public int getAge(){ return this.age; } public String getName(){ return this.name; } public void setAge(int age){ this.age = age; } public void setName(String name){ this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(this.name, this.age); } }
上面说到HashSet是无序的结构,若是咱们想要使用hashSet,可是又想它有序,该怎么办?在Set中提供了另外一个实现,LinkedHashMap。它的底层是一个Hash表和一个链表,Hash表用来存储真正的数据,而链表用来存储元素的顺序,这样就结合了两者的优先。学习
Map是一个双列的容器,一个节点存储了两个值,一个是元素的键,另外一个是值。其中Key 和 Value既能够是相同类型的值,也能够是不一样类型的值。Key和Value是一一对应的关系。一个key只能对应一个值,可是多个key能够指向同一个value,有点像数学中函数的自变量和值的关系。ui
Map经常使用的实现类有: HashMap和LinkedHashMap。this
经常使用的方法有:指针
void clear()
: 清空集合boolean containsKey(Object key)
: map中是否包含对应的键V get(Object key)
: 根据键返回对应的值V put(K key, V value)
: 添加键值对boolean isEmpty()
: 集合是否为空int size()
: 包含键值对的个数针对列表类型的,元素顺序固定,咱们可使用循环依据索引进行遍历,好比
for(int i = 0; i < list.size(); i++){ String s = list.get(i); }
而对于Set这种不关心元素的顺序的集合来讲,不能再使用索引了。针对单列集合,有一个迭代器接口,使用迭代器能够实现遍历
迭代器能够理解为指向集合中某一个元素的指针。使用迭代器能够操做元素自己,也能够根据当前元素寻找到下一个元素,它的经常使用方法有:
boolean hasNext()
: 当前迭代器指向的位置是否有下一个元素E next()
: 获取下一个元素并返回。调用这个方法后,迭代器指向的位置发生改变使用迭代器的通常步骤以下:
iterator()
返回一个迭代器hasNext
方法,判断集合中是否还有元素须要遍历next
方法,找到迭代器指向的下一个元素//假设set是一个 HashSet<String>集合 Iterator<String> it = set.iterator(); while(it.hasNext()){ Stirng s = it.next(); }
索引和迭代器的方式只能遍历单列集合,像Map这样的多列集合不能使用上述方式,它有额外的方法,主要有两种方式
针对第一种方法,Map中有一个 keySet()
方法。这个方法会获取到全部的key值并保存将这些值保存为一个新的Set返回,咱们只要遍历这个Set并调用 Map的get方法便可获取到对应的Value, 例如:
// 假设map 是一个 HashMap<String, String> 集合 Set<String> kSet = map.keySet(); Iterator<String> key = kSet.iterator(); while(it.hasNext()){ String key = it.next(); String value = map.get(key); }
针对第二种方法,能够先调用 Map的 entrySet()
获取一个Entry
结构的Set集合。Entry
中保存了一个键和它对应的值。使用结构中的 getKey()
和 getValue()
分别获取key和value。这个结构是定义在Map中的内部类,所以在使用的时候须要使用Map这个类名调用
// 假设map 是一个 HashMap<String, String> 集合 Set<Map.Entry<String,String>> entry = map.entrySet(); Iterator<Map.Entry<String, String>> it = entry.iterator(); while(it.hasNext()){ Map.Entry<String, String> me = it.next(); String key = me.getKey(); String value = me.getValue(); }
在上述遍历的代码中,不论是使用for或者while都显得比较麻烦,咱们能像 Python
等脚本语言那样,直接在 for
中使用迭代吗?从JDK1.5 之后引入了for each写法,使Java可以直接使用for迭代,而不用手工使用迭代器来进行迭代。
for (T t: set);
上述是它的简单写法。
例如咱们对遍历Set的写法进行简化
//假设set是一个 HashSet<String>集合 for(String s: set){ //TODO:do some thing }
咱们说使用 for each写法主要是为了简化迭代的写法,它在底层仍然采用的是迭代器的方式来遍历,针对向Map这样没法直接使用迭代的结构来讲,天然没法使用这种简化的写法,针对Map来讲须要使用上述的两种遍历方式中的一种,先转化为可迭代的结构,而后使用for each循环
// 假设map 是一个 HashMap<String, String> 集合 Set<Map.Entry<String, String>> set = map.entrySet(); for(Map.Entry<String, String> entry: set){ String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + "-->" + value); }
在上述的集合中,咱们已经使用了泛型。
泛型与C++ 中的模板基本相似,都是为了重复使用代码而产生的一种语法。因为这些集合在建立,增删改查上代码基本相似,只是事先不知道要存储的数据的类型。若是没有泛型,咱们须要将全部类型对应的这些结构的代码都重复写一遍。有了泛型咱们就能更加专一于算法的实现,而不用考虑具体的数据类型。
在定义泛型的时候,只须要使用 <>中包含表示泛型的字母便可。常见的泛型有:
<>
中可使用任意标识符来表示泛型,只要符合Java的命名规则便可。使用 T
或者 E
只是为了方便而已,好比下面的例子
public static <Element> void print(Element e){ System.out.println(e); }
固然也可使用Object 对象来实现泛型的重用代码的功效,在对元素进行操做的时候主要使用java的多态来实现。可是使用多态的一个缺点是没法使用元素对象的特有方法。
泛型能够在类、接口、方法中使用
在定义类时定义的泛型能够在类的任意位置使用
class DataCollection<T>{ private T data; public T getData(){ return this.data; } public void SetData(T data){ this.data = data; } }
在定义类的时候定义的泛型在建立对象的时候指定具体的类型.
也能够在定义接口的时候定义泛型
public interface DataCollection<T>{ public abstract T getData(); public abstract void setData(T data); }
定义接口时定义的泛型能够在定义实现类的时候指定泛型,或者在建立实现类的对象时指定泛型
public class StringDataCollectionImpl implements DataCollection<String>{ private String data; public String getData(){ return this.data; } public void SetData(String data){ this.data = data; } } public interface DataCollection<T> implements DataCollection<T>{ private T data; public T getData(){ return this.data; } public void SetData(T data){ this.data = data; } }
除了在定义类和接口时使用外,还能够在定义方法的时候使用,针对这种状况,不须要显示的指定使用哪一种类型,因为接收返回数据和传入参数的时候已经知道了
public static <Element> Element print(Element e){ System.out.println(e); return e; } String s = print("hello world");
在使用通配符的时候可能有这样的需求:我想要使用泛型,可是不但愿它传入任意类型的值,我只想要处理继承自某一个类的类型,就好比说我只想保存那些实现了某个接口的类。咱们固然能够将数据类型定义为某个接口,可是因为多态的这一个缺陷,实现起来总不是那么完美。这个时候可使用泛型的通配符。
泛型中使用 ?
做为统配符。在通配符中可使用 super
或者 extends
表示泛型必须是某个类型的父类或者是某个类型的实现类
class Fruit{ } class Apple extends Fruit{ } class Bananal extends Fruit{ } static void putFruit(<? extends Fruit> data){ }
上述代码中 putFruit
函数中只容许 传递 Fruit
类的子类或者它自己做为参数。
固然也可使用 <? super T>
表示只能取 T类型的父类或者T类型自己。