------<ahref="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------
我们经过之前的学习,知道一个类可以创建很多的对象,对象多了之后就需要对它们进行整理归纳,把这些对象存储起来。
集合类就是专门用来存储对象的工具。数组也是用来存储的,但集合类与数组的不同在于:数组是固定长度的,集合是可变长度的。数组里面只能存同一种类型的对象,而集合里面只要是对象就行。
集合就是对它里面所存储的对象进行的向上抽取,而很多集合再向上抽取就是集合框架。
下图是集合框架构成及分类关系图。
以上是比较常见的容器划分。
为什么会出现这么多的容器呢?
因为每一个容器对数据的存储方式都有不同。
这个存储方式称之为:数据结构。
集合框架大体分为Collection和Map两大类。
先来看Collection集合以及它的主要子类。
Collection(单列集合)
List有序
ArrayListLinkedList Vector
Set无序
HashSet TreeSet
按照以前的经验先从最上面的Collection根接口学习一些共性的东西,过程中可以创建子类对象,因为父类可能是抽象类创建不了对象,而且一般子类功能更全一些。
首先Collection是接口,因此没有构造方法,只有一般方法。又因为它是容器,所以应该会具备增删改查的功能。
【注意】开始编写代码之前要导入 import java.util.*;
这时编译会出现提示,有两个注意,不管也没事,是1.5版本加入了泛型之后产生的安全隐患,java只是友好地提示一下。但如果编译时底下显示的是错误那就是代码有问题。
首先我们来看add(Objectobj);方法,它能够把一个对象添加到集合里面去,它的参数类型是Object以便于接收任意类型的对象(多态)。
【需要明确的是】用add方法请注意:集合在内存中中存放的不可能是对象实体,否则的话集合这个对象会非常臃肿。而且已经建立好的对象还要挪位置。记住:集合和数组里面存的都是对象的引用(地址)。
集合可以直接打印,java自定义会把集合中的元素封装在中括号里,用逗号分隔开来打印出来。
集合中的一些常用方法
判断元素是否存在
boolean contains(目标对象);判断集合是否包含目标对象。
boolean isEmpty();判断该集合是否为空。
取交集
boolean al1.retainAll(al2); //取交集 运算结束后,al1中只会保留和al2中相同的元素。如果al1和al2没有相同的元素,al1会变成空[]。返回的布尔型是判断两个集合是否完全一样,不一样返回true,一样返回false。
boolean al1.removeAll(al2); //去交集
Iterator it = al.iterator();//获取迭代器,用于取出集合中的元素。
it.next()//利用迭代器获得下一个元素it.hasNext();如果还有下一个元素返回真,否则假。
迭代器
集合的取出元素的方式。
一般往集合里添加元素比较简单,定义一个add方法就可以了。但是从集合里面取出数据比较复杂以至于单一的功能不足以完成,而需要一连串动作,所以就将这些动作封装在一个类里面。而我们又要操作集合里的元素,因此使用这个类最简单的方式就是把这个类定义在集合内部,即内部类,这样取出方式就可以直接访问集合内容的元素。而每一个容器的数据结构不同,所以取出的动作细节也不尽一样,但他们都有一些共性内容,如判断是否存在元素,以及取出,那么我们可以将其共性抽取,产生一个Iterator的接口。具体实现的内部类定义在各个集合里面。而java给我们对外提供了一个方法iterator();来让我们能使用这个内部类来取出元素。
比喻:游戏厅的抓玩具机:爪子定义在容器里,用来抓取里面的玩具元素,每个机器爪子各不相同,但都有一定共性,对外提供投币口,操纵杆来供我们使用。
实际开发中最常用的两个Collection的子类接口:
List和Set
List:元素是有序的,元素可以重复。因为该集合体系有索引。
Set:元素是无序的,元素不可以重复。因为该集合体系没有索引。
List集合的特有方法:凡是可以操作角标的方法都是该体系特有的方法。
增
add(index,element);
addAll(index,Collection);
删
remove(index);
改
set(index,element); 返回的是被替换的那个元素。
查
get(index);
subList(from,to);
listIterator();
【注意】在list的子类中如果使用iterator迭代从容器里面获取元素的同时使用集合对象的方法(如add)操纵同一集合中的元素时就会发生ConcurrentModificationException并发修改异常,很好理解,迭代器在生成的时候只知道当时容器里面有多少个元素。此后再要添加新的它无法得知,如果要通过集合的方法删除元素那就更不行了,迭代器本以为有,结果取不着。因此我们在迭代过程中不能使用集合的方法,只能使用迭代器的方法。
Iterator这个迭代器只能做三个动作next();hasNext();和remove();没有添加等高级动作。
需要注意的是:迭代器里面有个指针,建立迭代器的时候,指针在第一个元素的前面,每次获取完next()指针都向后移动一位到下一个元素与下下个元素之间,最后移到最后一个元素的后面结束。
List集合特有的迭代器。ListIterator是Iterator的子接口。该接口只能通过List集合的listIterator()方法获取。这个迭代器出现后可以对集合进行在迭代过程中的增(add)删(remove)改(remove)查(previous, next)。
ListIterator迭代器比Iterator迭代器还多具备previous()与hasPrevious()方法,它们分别与next()和hasNext()对应,很简单。
ArrayList:
底层的数据结构使用的是数组结构。有角标。特点:查询、修改速度很快。而增加删除很慢,因为要从中插入的话需要把后面所有元素都往后挤,删除也是同理。因此元素越多,越慢。该类是从JDK 1.2版本以后出现的,它是线程不同步的。效率高,而且可以自己加锁。
集合长度可变的真相:
ArrayList使用的是可变长度数组:数组长度是不可变的,而集合长度是可变的。ArrayList()在构造的时候初始化一个容量为10的空数组,当添加长度超出时会新建一个长度等于原有长度150%的新数组(Vector是200%延长),再把原数组里的内容copy到新数组中,并在之后添加上新加入的元素。可变长度数组是通过不断new数组产生的。
这个类最常用,当不知道用哪种容器的时候一般都选择ArrayList。当增删不多,查找修改较多时首选不犹豫。
LinkedList:
底层使用的链表数据结构。每一个元素记住它下一个元素的信息,像链子一样连接起来。特点:增删快,查改慢。(要改的话首先要查到),该类也是线程不同步的。
特有方法:
addFirst();//新加入的元素放在第一个
addLast();//新加入的元素放在最后一个
getFirst();//获取第一个元素
getLast();//获取第最后一个元素
获取元素,但不删除。//如果此列表为空会发生NoSuchElementException
removeFirst();
removeLast();
获取元素,同时删除元素。长度改变。//如果此列表为空会发生NoSuchElementException
JDK1.6出现了新方法来替代add,get和remove 和
offerFirst();
offerLast();
peekFirst();
peedLast();
//如果此列表为空会返回null。
pollFirst();
pollLast();
//如果此列表为空会返回null。
Vector:底层是数组数据结构。Vector是1.0版本中的元老级成员,以前的开发者用的都是vector,那时集合框架Collection还不存在。Vector是线程同步的。效率低,超慢,被ArrayList替代。
枚举Enumeration是Vector特有的取出方式,早期只有这种方式,其实枚举和迭代时一样的,应该优先使用迭代。因为枚举的名称以及方法名称都过长,所以被迭代器取代了。
以上概念很简单,这里我们通过做题进一步反映集合类的概念。
练习1
用LinkedList模拟一个堆栈或者队列数据结构。
堆栈:先进后出 如同一个杯子
队列:先进先出 First In First Out FIFO 如同一个管子
代码如下:
import java.util.*;
class MyQueue//用LinkedList模拟一个堆栈或者队列数据结构。
{
private LinkedList link;
MyQueue()
{
link = new LinkedList();
}
public void myAdd(Object obj)
{
link.offerFirst(obj);
}
public Object myGet()
{
return link.pollLast();//堆栈的话只要把这里的Last改为First即可。
}
public boolean isNull()
{
return link.isEmpty();
}
}
class LinkedListTest
{
public static void main(String[] args)
{
MyQueue mq = new MyQueue();
mq.myAdd("java-01");
mq.myAdd("java-02");
mq.myAdd("java-03");
mq.myAdd("java-04");
for (; !mq.isNull(); )
sop(mq.myGet());
}
public static void sop(Object obj)
{
System.out.println(obj);
}
}
练习2
去除ArrayList中的重复元素。
代码如下:
import java.util.*;
class ArrayListTest2//去除ArrayList中的重复元素。
{
public static ArrayList singleElement(ArrayList al)
{
ArrayList newAl = new ArrayList();
Iterator it = al.iterator();
for (; it.hasNext(); )
{
Object obj = it.next();
if (!newAl.contains(obj))
newAl.add(obj);
}
return newAl;
}
public static void main(String[] args)
{
ArrayList al = new ArrayList();
al.add("java 01");
al.add("java 02");
al.add("java 01");
al.add("java 02");
al.add("java 01");
al.add("java 01");
al.add("java 02");
al.add("java 02");
al.add("java 03");
sop(al);
al = singleElement(al);
sop(al);
}
public static void sop(Object obj)
{
System.out.println(obj);
}
}
练习3
将自定义对象作为元素存储到ArrayList集合中,并去除重复元素。
比如:存人对象,同名同龄(所有属性相同),视为同一个人。为重复元素。
代码如下:
import java.util.*;
class ArrayListTest3//将自定义对象作为元素存储到ArrayList集合中,并去除重复元素。比如:存人对象,同名同龄(所有属性相同),视为同一个人。为重复元素。
{
public static void main(String[] args)
{
Object o1 = new Person("张七",27);
Object o2 = new Person("张七",27);
sop(o1.equals(o2));//多态里面如果子类Person有一个方法父类Object没有,多态的o1想要使用必须向下转型,而如果子类有个方法把父类复写了,对象(如Object o1)在调用这个方法的时候使用的还是子类复写完的方法不用向下转型)
ArrayList al = new ArrayList();
al.add(new Person("张三",21));
al.add(new Person("张四",22));
al.add(new Person("张五",24));
al.add(new Person("张六",22));
al.add(new Person("张七",27));
al.add(new Person("张七",27));
sop("remove 张五:"+al.remove(new Person("张五",24)));//remove在底层也依赖equals方法判断要移除的元素是否等于自己集合中的某一个元素,逐个判断,如果不复写,这里新建的对象内存地址值不会和上面建立的"张五"相等,因此返回假,不会删除"张五",而经过复写之后的新方法,判断里面的值完全一样,返回真,就会删除掉。
for (Iterator it = al.iterator(); it.hasNext(); )
{
Person p = (Person)(it.next());//向下转型,因为next()方法返回的是Object类的对象,而此类对象没有getName()等具体子类方法。多态特性。
sop(p.getName()+"...."+p.getAge());
}
al = singleElement(al);//这个方法里面contains很重要,往下看
sop("-----------华丽丽的分界线-----------");
for (Iterator it = al.iterator(); it.hasNext(); )//再打印一遍,看差别
{
Person p = (Person)(it.next());
sop(p.getName()+"...."+p.getAge());
}
}
public static ArrayList singleElement(ArrayList al)
{
ArrayList newAl = new ArrayList();
Iterator it = al.iterator();
for (; it.hasNext(); )
{
Object obj = it.next();
if (!newAl.contains(obj))//List集合的contains方法在底层调用的是obj的equals方法(其他集合不一定),逐一和newAl里面的每一个元素对象进行equals对比(obj.equals(元素x)),而Object类的equals方法比较的其实是两个对象的地址,在我们的例子里不能把雷同元素删除掉。因此我们要把Person类里面的equals方法复写成我们所需要的形式,也就是比较内容。注意:如果obj不为空时newAl为空的话不用比较,contains直接返回假。
newAl.add(obj);
}
return newAl;
}
public static void sop(Object obj)
{
System.out.println(obj);
}
}
class Person
{
private String name;
private int age;
Person(String name, int age)
{
this.name = name;
this.age = age;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
public boolean equals (Object obj)//Person类里面复写Object的equals方法,这里传入的参数必须是Object类的,否则无法复写父类,而是重载。
{
if (!(obj instanceof Person))//判断obj是否是Person类的实例
return false;
Person p = (Person)obj;
System.out.println(this.name+"......"+p.name);//因为传入参数Obj里面没有name和age属性,所以如果直接写name默认是this.name。
return this.name.equals(p.name) && this.age == p.age;//这里的equals是字符串的equals方法,已经在String里面就复写了Object的同名方法。
}
}
在上面的例子中我们看到这段代码:newAl.contains(obj),这里需要讲一下。
List集合的contains方法在底层调用的是其实是Object类的equals方法(List集合是这样做的,其他集合不一定),让目标对象逐一和newAl集合里面的每一个对象进行equals对比(obj.equals(newAl元素x)),而Object类的equals方法比较的其实是两个对象的地址,而在我们的例子里就算元素是雷同的地址值也不一样,因为每个都是不同的对象,因此不能达到把雷同元素删除掉的目的。所以我们要把Person类里面的equals方法复写成我们所需要的形式,也就是比较内容。
【注意】如果obj不为空时newAl为空的话不用比较,contains直接返回假。
另外boolean remove(Object obj)底层也调用的equals()方法,逐个判断要删除的元素是否是自己的某一个元素。
List集合中的remove和contains都依赖equals但其他集合不一定。
Set集合
该集合元素是无序的(存入和取出的顺序不一定一致),元素不可重复。
Set集合的功能和Collection是一致的。
常见的子类有HashSet和TreeSet。
Set集合取出只有一种方式:迭代器。
HashSet:底层数据结构是哈希表。线程是非同步的。存储是按照哈希表(可以看做内存地址值)大小顺序的,因此可能会与存储顺序不一致。因为不能存储重复的元素,所以在存储的时候底层会做两步判断来保证元素的唯一性:
1. 通过元素的方法hashCode()算出哈希值是否相等(哈希值可以通过复写Object的hashCode)如果哈希值相等,还可以往里存,但在哈希表中两个元素都存储在同一个哈希值底下。
2. 当元素的哈希值相同时,使用元素的equals方法判断是否是同一个元素(用的还是Object的equals方法),如果是,不能往里存。
如果我们自行建立比较有针对性的hashCode会更高效一些,否则地址值都不一样,所有对象都能存进去。所以一般我们开发的时候要建立对象往集合里面存一般都会复写hashCode和equals
【注意】对于判断元素是否存在contains,以及删除remove等操作,依赖的方法是元素的hashCode(先)和equals(后)方法。与前面一样。
ArrayList判断元素是否存在以及删除元素只依赖equals方法,而HashSet先依赖hashCode再依赖equals,这就是哈希表的特点,因为数据结构不同。
hashCode的实现原理:(拔高,反射类的最后一个例子里也有体现)
一个元素要进集合,具体分为如下几部:首先算出它的hashCode,然后用hashCode取模,得出结果后,根据那个结果放到HashSet的指定区域中(比如说拿32取模,那么结果就有32种,就分了32个区)再来一个新的元素不用每个元素挨个比了,直接算出hashCode取模的结果,然后根据这个结果上指定的区域寻找,再在那个确定了的区域里和其中的元素挨个比,这样相当于缩小了32倍的查找范围能提高效率。
还有一点非常重要注意:
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算hashCode值的字段了。否则,对象修改后的哈希值与最初存储进HashSet集合中的哈希值就不同了,这种情况下检索对象返回不出对象的结果,这会导致无法从HashSet集合中单独删除当前对象(要删除肯定先得找到,可是我们存储的时候是按照A哈希值存进去的,集合因此把那个哈希值与那个对象关联起来,可是当我们修改了参与哈希值运算的参数的时候,对象的哈希值就变化了,而这个变化仅仅在对象上体现,没有反映到集合中,这时我们再拿着这个对象去查找的话系统会根据新算出的哈希值去查集合中过时的哈希表,肯定是找不到对应的哈希值,因此就相当于没有找到那个对象,所以删不了),从而造成内存泄露。(有的资源以后都再也不会被使用了,可程序往下运行,它还没有被释放掉,浪费内存)。
与hashCode有关的集合才需要考虑上面的,否则如果像是ArrayList跟这个没关系。
TreeSet:强制对Set集合中的元素按照一定顺序进行排序,在集合中排列的方式也是按照TreeSet定义的排序的顺序。,
TreeSet排序的第一种方式:
让元素自身具备比较性,必须实现Comparable接口(此接口强行对实现它的所有类的对象进行整体排序,排列的顺序就是它们的自然顺序,也就是默认顺序,TreeSet就是以这个顺序往集合里添加元素的。),并覆盖该接口中compareTo方法。
往TreeSet里面添加元素时,会自动在底层调用每个新添元素的compareTo方法,与集合原有元素进行比较,返回值是正数负数或者0。(当TreeSet集合为空时不调用)。Java会记住每个比较结果,所以新元素不用与集合中所有元素都比一遍,比如说集合中有2,3,4,要新进来个5,当与4比完发现更大的话就自动往集合里面添加,不会与2,3比,因为它们经过比较已经比4要小了。底层数据结构是二叉树(也叫红黑树):减少比较次数,提高比较性能。保证元素唯一性的依据:compareTo方法return 0(表示两个元素相同),那样这两个元素就被视为完全相同的元素,不能在集合中共存。如果想用TreeSet集合删除元素或者判断是否包含某个元素,使用的都是compareTo。
二叉树原理
如图,
二叉树最上面的那个是第一个参与比较的对象,也就是第二个输入的,以后每进来个新的元素都会和它比,比它大的在右边,小的放左边,一层一层按照这个原则进行对比,直到新加元素找到自己在二叉树中的位置。但是当二叉树变大,变得不平衡后速度也会慢,因此当超过一定长度后,二叉树的第一个比较对象会自动从22转换到一个更加折中的值来确保比较速率。
String类默认的compareTo方法,比较两个字符串的ASCII表,从前往后比较,并返回差值。
练习
需求:往TreeSet集合中存储自定义对象学生。想按照学生的年龄进行排序。
记住,排序时,当主要条件相同,一定要判断一下次要条件。
代码如下:
import java.util.*;
class TreeSetDemo
{
public static void main(String[] args)
{
TreeSet ts = new TreeSet();
ts.add(new Student("lisi02",22));
ts.add(new Student("lisi03",23));
ts.add(new Student("lisi04",24));
ts.add(new Student("lisi05",25));
ts.add(new Student("lisi06",26));
ts.add(new Student("lisi02",22));
ts.add(new Student("lisi09",22));
sop("-----------华丽丽的分界线-----------");
for (Iterator it = ts.iterator(); it.hasNext(); )
{
Student s = (Student)it.next();
sop(s.getName()+"........"+s.getAge());
}
}
public static void sop(Object obj)
{
System.out.println(obj);
}
}
class Student implements Comparable
{
private String name;
private int age;
Student(String name, int age)
{
this.name = name;
this.age = age;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
public int compareTo(Object obj)
{
if(!(obj instanceof Student))
throw new RuntimeException("不是学生对象");
Student s = (Student)obj;
System.out.println(this.name+"...compareTo..."+s.name);
if (this.age>s.age)//这里面如果不是为了判断在年龄相同情况下名称是否相同,更简单的做法是直接 return (this.age-s.age);
return 1;
if (this.age==s.age)
{
System.out.println(this.name.compareTo(s.name));//调用的是String类默认的compareTo方法,比较两个字符串的ASCII表,从前往后比较,并返回差值。
return this.name.compareTo(s.name);
}
return -1;
/*这个判断方法更加高明一些
int x = new Integer(this.age).compareTo(new Integer (s.age));//调用的是Integer类(而不是int型变量)默认的compareTo方法,比较两个数,从前往后比较,并返回差值。
if (x==0)
x = this.name.compareTo(s.name);
return x;
*/
}//如果里面只有一句 return 1的话就是不管谁跟谁比,后来的都比先来的打,那么打印结果就是我们的输入顺序,如果想要反序,把return 1改为return -1即可。如果是return 0,那就是说所有新添元素和原有元素都一样,那么集合里面只能有第一个添加进来的元素。
}
TreeSet排序的第二种方式:
当元素自身不具备比较性时,或者具备的比较性不是所需要的。这时就需要让集合自身具备比较性,在集合初始化时,就有了比较方式。 因此定义了比较器,将比较器对象作为参数传递给TreeSet集合的构造函数:定义一个类(比较器),实现Comparator接口,覆盖compare方法(里面有两个传递参数,比较两者)。其实底层比较的方法相同都是二叉树,并以return 0来判断是否相同。
注意:集合的比较器与元素自身的比较方式并存时,以比较器为主。真实开发中,多用比较器。
接口就是对外的功能扩展,对外的规则。真实开发也要多用接口,接口提升扩展性,便于升级维护。
练习:往TreeSet里面存字符串,按照字符串长度排序。
代码如下:
import java.util.*;
class TreeSetTest
{
public static void main(String[] args)
{
TreeSet ts = new TreeSet(new StringLengthComparator());
ts.add("i am silly b");
ts.add("sb");
ts.add("ssbb");
ts.add("i b");
ts.add("i b");
ts.add("ib");
for (Iterator it = ts.iterator(); it.hasNext(); )
{
sop(it.next());
}
}
public static void sop(Object obj)
{
System.out.println(obj);
}
}
class StringLengthComparator implements Comparator
{
public int compare(Object o1, Object o2)
{
if (!(o1 instanceof String && o2 instanceof String))
throw new RuntimeException("Comparator接收到非字符串对象");
String s1 = (String)o1, s2 = (String)o2;
int x = new Integer(s1.length()).compareTo(new Integer(s2.length()));
if (x==0)
x=s1.compareTo(s2);
return x;
}
}
泛型
JDK1.5版本以后出现的新特性,用于解决安全问题,是一个类型安全机制。
好处
1. 将运行时出现的问题ClassCastException转移到了编译时期。方便于程序猿解决问题,让运行时问题减少,更安全。加了泛型之后编译时就不会出现注意提示了。
2. 避免了强制转换的麻烦。
3. 泛型的格式:通过<>来定义要操作的引用数据类型。
在使用java提供的对象时,什么时候写泛型?在集合框架中很常见。只要见到<>就要定义泛型。<>就是用来接收类型的。当使用集合时,将集合中要存储的数据类型作为参数传递到<>中即可,类似于函数中的传递参数。
Comparable 和Comparator也要写<泛型参数>避免强制转换的麻烦,但是equals方法不写,因为我们复写的是Object的equals方法,它没有泛型。equals方法必须得做转换,而如果我们自定义equals(X)括号里传入的类不是Object时不算复写,只能算是重载,相当于两个函数,但是在被底层调用的时候还是使用的equals(Object obj),我们自定义的无效。
所以我们在定义一个类时,一般都要顺便复写hashCode() equals() compareTo() toString(),并使用泛型的方法。
泛型代码示例:
import java.util.*;
class GenericDemo2 //用泛型方法优化完的TreeSetDemo2.java
{
public static void main(String[] args)
{
MyComparator mc = new MyComparator();
TreeSet<Student> ts = new TreeSet<Student>(mc);//当两种比较方式共存时,以比较器为主。
ts.add(new Student("lisi01",11));
ts.add(new Student("lisi02",12));
ts.add(new Student("lisi03",13));
ts.add(new Student("lisi04",14));
ts.add(new Student("lisi05",15));
ts.add(new Student("lisi00",12));//不纯重复的
ts.add(new Student("lisi02",10));//纯重复的
ts.add(new Student("lisi02",12));//纯重复的
for (Iterator<Student> it = ts.iterator(); it.hasNext(); )
{
Student s = it.next();
sop(s.getName()+"........"+s.getAge());
}
}
public static void sop(Object obj)
{
System.out.println(obj);
}
}
class Student implements Comparable<Student>
{
private String name;
private int age;
Student(String name, int age)
{
this.name = name;
this.age = age;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
public int compareTo(Student s)
{
int x = new Integer(this.age).compareTo(new Integer (s.age));
if (x==0)
x = this.name.compareTo(s.name);
return x;
}
}
class MyComparator implements Comparator <Student>
{
public int compare(Student s1, Student s2)
{
int x = s1.getName().compareTo(s2.getName());
if (x == 0)
x = new Integer(s1.getAge()).compareTo(new Integer(s2.getAge()));
return x;
}
}
泛型类
当类中要操作的引用数据类型不确定的时候(基本数据类型必须确定),早期定义Object来完成扩展(现在的equals方法也是)。现在定义泛型来完成扩展。
有了泛型就不用强转了。并且会把错误转移到编译时期。
说白了:类操作的引用数据类型不确定,交给调用者定义。但不管什么类型,都按照类的操作方法。
格式:class 类名 <T>
代码示例:
import java.util.*;
class GenericDemo3
{
public static void main(String[] args)
{
Tool<Student> t = new Tool<Student>();
t.setObject(new Student());
Student s = t.getObject();//有了泛型就不用强转了。并且会把错误转移到编译时期。
}
}
class Tool<E>//操作的引用数据类型不确定,交给调用者定义。但不管什么类型,都按照我的操作方法。
{
private E e;
public void setObject(E e)
{
this.e = e;
}
public E getObject()
{
return e;
}
}
class Student
{
}
class Worker
{
}
泛型方法:
泛型类的对象明确好要操作的具体引用数据类型后,它的方法必须使用那一种的类型,所有要操作的类型因此也就固定了。
而如果想让不同方法可以操作不同类型,而且类型还不确定,就可以将泛型定义在方法上。
格式:把泛型放在修饰符(如static)的后面,返回类型(如void)的前面。
如public<E> void show(E e)
泛型方法的定义在有的时候比泛型类要方便些。
代码示例:
import java.util.*;
class GenericDemo4
{
public static void main(String[] args)
{
Tool t = new Tool();//泛型方法的定义在有的时候比泛型类要方便些,否则如果要打印字符类又要打印Integer类就得建立两个Tool对象,一个用String类型一个用Integer类型。
t.print("haha");
t.print(new Integer(2));
t.show(4);
}
public static void sop(Object obj)
{
System.out.println(obj);
}
}
class Tool
{
public<E> void print(E e)
{
sop("print:"+e);
}
public<E> void show(E e)//这个e是局部变量,只在此方法中有效,跟上面的e不是一回事
{
sop("show:"+e);
}
public static void sop(Object obj)
{
System.out.println(obj);
}
}
共存&静态方法泛型:
泛型类和泛型方法可以在一个类里面共存。
静态方法不可以访问类上定义的泛型,因为静态先加载,此时对象还没建立,静态方法不知道它要操作什么类型。
如果静态方法操作的引用数据类型不确定,可以将泛型定义在其方法上。
如:public static<T> void method(T t)
静态只能用泛型方法,不能用泛型类。
泛型接口
格式:interface 接口名 <T>,定义方法与泛型类一致,但是需要注意的是,我们在定义实现这个泛型接口的子类的时候,如果能确定所要操作的参数,就可以直接将确定的泛型填入,如果仍然无法确定是什么参数,就将泛型保留,交给调用者创建具体对象时定义。
代码示例:
import java.util.*;
interface Inter<T>//泛型定义在接口上
{
void method(T t);
}
/*
class InterImpl implements Inter<String>//第一种方法,能确定操作的类型,所以直接在类里面传入固定好的参数
{
public void method(String s)
{
System.out.println("method:"+s);
}
}
*/
class InterImpl<Q> implements Inter<Q>//第二种方法,不能确定操作的类型,所以保留给调用者定义
{
public void method(Q q)
{
System.out.println("method:"+q);
}
public<E> void print(E e)
{
System.out.println("print:"+e);
}
}
class GenericDemo6
{
public static void main(String[] args)
{
InterImpl<String> i = new InterImpl<String>();
i.method("hahahaha");
i.print(1111);
InterImpl<Integer> i2 = new InterImpl<Integer>();
i2.method(123);
i2.print(2222);
}
}
泛型限定
泛型中<?>表示不明确类型,?是通配符(占位符),表明这里存在占位符,但不确定占位符具体是什么。不明确具体类型的情况下也可以用?表示。占位符的缺点在于无法使用类型方法,但如果用<T>还可以调用对象。而泛型的缺点是无法直接使用某个具体对象的特有功能,如length();
代码示例:
import java.util.*;
class GenericDemo7
{
public static void main(String[] args)
{
ArrayList<String> al = new ArrayList<String>();
al.add("a1");
al.add("a2");
al.add("a3");
al.add("a4");
ArrayList<Integer> al2 = new ArrayList<Integer>();
al2.add(5);
al2.add(7);
al2.add(9);
al2.add(5);
// ArrayList al3 = new ArrayList();
// al3.add(5);
// al3.add(7);
// al3.add("abcde");
// al3.add(5);
printColl(al);
printColl(al2);
// printColl(al3);
}
public static void printColl(ArrayList<?> al)
{
for (Iterator<?> it = al.iterator(); it.hasNext(); )
sop(it.next());//也可以用.hashCode()调用这个对象的特有方法
}
public static void sop(Object obj)
{
System.out.println(obj);
}
}
泛型限定还有如下的一些写法:
? extends E: 可以接收E类型或者E的子类型。 相当于限定了上限。(例如Collection的addAll(Collection<? extends E> c)方法可以添加一个存储着它们子类的集合)
【注意】泛型里面不存在多态,没有如下写法:
TreeSet<Father> ts = new TreeSet<Son>();
左右两边的类型必须一致。不能用一个动物圈的类类型变量指向一个存的都是猪的猪圈集合。虽然两个集合内部存储的元素有继承关系,但是这两个集合之间没有继承关系,不符合多态特性。比如说A类卡车规定里面拉的都是建材,B类卡车规定里面拉的都是木板,但是我们不能用A类卡车的类类型变量指向B类卡车的一个具体对象,它们二者没有关系,至少没有定义继承关系。
这与上面addAll方法不一样,那个相当于让一个A类卡车能把一个B类的货都装进来,只要B类卡车上的货都是A类卡车规定货物的子类:我可以把一车木板都卸到一个只允许装建材的车上。
? extends E可以看作是父类对子类的向下兼容,用的稍多一些。
【注意】这里的extends不仅表示继承,也表示实现,也就是说后面的E也可以是一个接口。
? super E: 可以接收E的父类型。 相当于限定了下限。(例如Comparator Comparable两个接口,子类也可以使用父类的比较器,比如说学生和工人都继承自人,两个类都要按年龄排序,那它们就不用每个单独定义一个比较器了,全部使用父类的即可)
? super E这个是给一个个子类预留让它们都可以使用父类的内容用的,只要里面使用的方法是所有兄弟子类共同继承自父类的即可,这样我们即使新创建一个子类就能直接使用同原来的方法。
定义的时候限定,明确泛型的规则,用的时候只要往上传符合要求的对象即可,不用再写<>了。
【运用泛型很重要的一点】要明确代码中哪部分是定义,哪部分是使用,以及有没有必要应用泛型,否则有的时候会出现我能确定你传入什么类型的数据了,却还定义泛型假装自己不知道的这种脱裤子放屁行为。这是为了泛型而泛型,不可取,我们应只在需要的时候泛型。
代码示例:
import java.util.*;
class GenericDemo8
{
public static void main(String[] args)
{
TreeSet<Person> ts1 = new TreeSet<Person>(new MyComp());//<? super E>
ts1.add(new Person("人类1"));
ts1.add(new Person("人类2"));
ts1.add(new Person("人类3"));
ts1.add(new Person("人类4"));
TreeSet<Student> ts2 = new TreeSet<Student>(new MyComp());//<? super E>
ts2.add(new Student("学生1"));
ts2.add(new Student("学生2"));
ts2.add(new Student("学生3"));
ts2.add(new Student("学生4"));
TreeSet<Worker> ts3 = new TreeSet<Worker>(new MyComp());//<? super E>
ts3.add(new Worker("工人1"));
ts3.add(new Worker("工人2"));
ts3.add(new Worker("工人3"));
ts3.add(new Worker("工人4"));
printColl(ts1);
printColl(ts2);
printColl(ts3);
}
public static void printColl(TreeSet<? extends Person> ts)
{
for (Iterator<? extends Person> it = ts.iterator(); it.hasNext(); )
sop(it.next().getName());
}
public static void sop(Object obj)
{
System.out.println(obj);
}
}
class MyComp/*<Person>*/ implements Comparator<Person>//<? super E>
{
public int compare(Person p1, Person p2)
{
return p1.getName().compareTo(p2.getName());//如果想被Person Student 以及Worker共同使用的话,这个方法必须是它们三个共有的。
}
}
class Person
{
private String name;
Person(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
class Student extends Person
{
Student(String name)
{
super(name);
}
public String getStr()
{
return "ab";
}
}
class Worker extends Person
{
Worker(String name)
{
super(name);
}
public String getStr()
{
return "abc";
}
}
以上大致介绍完了Collection集合,而与之相对应的还有Map集合,前者又叫单列集合,后者又称为双列集合。