今日任务
1、TreeSet介绍(掌握TreeSet集合的应用)
2、Comparable 接口介绍(掌握)
3、Comparator 比较器介绍(掌握)
4、Collection下的接口和实现类的总结
5、泛型技术(掌握泛型的基本使用)
1、TreeSet介绍(掌握)
1.1、TreeSet介绍
目前为止我们学习三个主要集合:
ArrayList:它的底层使用的可变数组,可以根据下标操作集合中的元素,可以重复,保证存取顺序。
LinkedList:它的底层是链表,有头有尾,可以根据头尾操作集合中的数据。也可以保存重复数据。
HashSet:它的底层是哈希表,不能保存重复数据,不保证存取顺序。
TreeSet:它是Set接口下的一个间接实现类。它可以保证对象元素唯一,存取无序,线程不安全,效率高,同时还可以对存放在其中的对象进行排序。
说明:
1)将数据存储在TreeSet集合中时就已经开始排序了;
2)这里说的自然排序可以暂时可以理解为升序排序(因为通过查看源代码发现底层就是升序排序);
1.2、TreeSet演示
分析和步骤:
1)创建一个TreeSetDemo类;
2)在这个类中定义一个method_1函数;
3)在method_1函数中创建TreeSet类的对象set;
4)使用对象set调用add函数向集合中添加几个字符串对象,并使用iterator迭代器遍历集合;
5)在定义一个method_2函数,在这个类中同样创建TreeSet类的对象set,并使用对象set调用add函数向集合中添加几个整数,并使用iterator迭代器遍历集合;
依次给集合存入整数20 31 10 13 23 5 51 5”
package cn.xuexi.set;
import java.util.Iterator;
import java.util.TreeSet;
/*
* TreeSet集合的演示
*/
public class TreeSetDemo {
public static void main(String[] args) {
//调用自定义函数
method_2();
}
public static void method_2() {
//创建集合对象 自然排序 升序
TreeSet set = new TreeSet();
//向集合中添加数据
set.add(20);
set.add(31);
set.add(10);
set.add(13);
set.add(23);
set.add(5);
set.add(51);
set.add(5);
//遍历集合
for (Iterator it = set.iterator(); it.hasNext();) {
//输出集合中的数据
System.out.println(it.next());
}
}
public static void method_1() {
//创建集合对象 自然排序 升序
TreeSet set = new TreeSet();
//向集合中添加数据
set.add("abc");
set.add("abcdef");
set.add("acc");
set.add("Abc");
set.add("abcd");
set.add("bbb");
set.add("bbb");
//遍历集合
for (Iterator it = set.iterator(); it.hasNext();) {
//输出集合中的数据
System.out.println(it.next());
}
}
}
输出结果分别是:
说明:这里先暂时把自然排序理解为升序排序,其实真正的自然排序和我们后面讲到的比较器Comparable接口有关。
通过之前所学的集合,我们发现不同的集合底层的数据结构是不一样的,从而导致存储数据方式也不一样了,那么我们接下来就会
来研究下为什么TreeSet集合不能保存重复元素和怎样进行排序的呢?
这显然和TreeSet底层的数据结构有关系。那么底层到底是个什么样的数据结构呢?
1.3、树结构介绍
通过查阅API我们得知TreeSet集合是基于TreeMap的实现,而TreeMap是基于二叉树(红黑树)结构,也就是说TreeSet集合的底层使用的二叉树(红黑树)结构。
树结构:它也是数据结构中的一种。在计算机领域中树结构指的是倒立的树。
树结构存储的数据,每个数据也需要节点来保存。
而TreeSet集合底层是二叉树的数据结构,什么是二叉树呢?
二叉树:每个节点的下面最多只能有2个子节点。
说明:最多表示一个节点下面可以有两个子节点或者一个子节点或者没有子节点。
在二叉树的根节点左侧的节点称为左子树,在根节点的右侧的节点称为右子树。
既然已经得知TreeSet集合底层是二叉树,那么二叉树是怎样存储数据的呢?是怎样保证存储的数据唯一并有序的呢?
二叉树的存储流程:
当存储一个元素的时候,如果是树的第一个元素,这个元素就作为根节点。
如果不是第一个元素,那么就拿要存储的元素与根节点进行比较大小:
大于根元素:就将要存储的元素放到根节点的右侧,作为右叶子节点。
等于根元素:丢弃。
小于根元素:就将要存储的元素放到根节点的左侧,作为左叶子节点。
总结:二叉树是通过比较大小来保证元素唯一和排序的。
如何遍历二叉树?
遍历二叉树有四种方式:前序遍历、中序遍历、后序遍历、按层遍历。
前序遍历,就是先访问 根节点------>左子树------>右子树
中序遍历,就是先访问 左子树------>根节点------>右子树
后序遍历,就是先访问 左子树------>右子树------>根节点
按层遍历,就是把一棵树从上到下,从左到右依次取出
而在TreeSet的底层使用中序遍历二叉树,即左----->根------>右。
二叉树的结构图如下图所示:
1.4、TreeSet集合存放自定义对象
案例:使用TreeSet集合存储Student对象。存储的Student对象要求按照年龄排序。
步骤和分析:之前我们一直向TreeSet集合中存储整数和字符串,接下来我们要存储自定义类Student对象。
1)定义一个Student类,在这个类中定义name和age两个属性,并创建构造函数给属性初始化值,并复写toString()函数;
2)定义一个测试类TreeSetDemo;
3)在TreeSetDemo类中创建TreeSet集合对象ts;
4)使用集合对象ts调用TreeSet集合中的add()函数分别向集合中添加几个学生对象,并遍历;
学生Student类代码如下:
package cn.xuexi.set;
/*
* 描述学生
*/
public class Student {
//属性
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
测试类TreeSetDemo代码如下:
package cn.xuexi.set;
import java.util.Iterator;
import java.util.TreeSet;
/*
* TreeSet集合的演示
*/
public class TreeSetDemo {
public static void main(String[] args) {
//创建集合对象
TreeSet ts = new TreeSet();
//向集合中添加数据
ts.add(new Student("张三",19));
ts.add(new Student("李四",20));
ts.add(new Student("王五",17));
ts.add(new Student("黑旋风",19));
//遍历集合
for (Iterator it = ts.iterator(); it.hasNext();) {
//输出集合中的数据
System.out.println(it.next());
}
}
}
上述代码运行结果会报如下错误:
分析异常的原因:
通过以上报异常错误原因大概知道我们要将自己定义的类Student的存储到TreeSet集合中的时候,发生自定义类Student不能被转换到Comparable比较器接口的异常,我们在代码中明明没有书写和Comparable接口相关的代码啊,那怎么会报这个异常呢?
这里我们需要查看一下TreeSet底层的源代码进行进一步分析。
2.5、TreeSet集合的源码分析
问题1:为什么TreeSet集合中存储的元素可以不重复呢?
TreeSet集合底层使用了二叉树结构,在存储元素时,拿存储的元素会和树结构中已经存在元素进行比较大小(结果有三种:大于、小于、相等)。当比较的结果相等时,表示该元素已经存在了,则不存储。
问题2:为什么TreeSet集合中存储的元素会排序呢?
在存储元素时,先拿要存储的元素和树结构中已经存在的元素进行比较大小,如果要存储的元素大于树结构中已存在的元素,则把要存储的元素存放比较元素的右边;如果要要存储的元素小于树结构中已存在的元素,则把要存储的元素放在左边。
分析TreeSet集合的源码查找为什么会报上述异常:
说明:
1)通过查看源代码得知当执行new TreeSet()集合时,相当于NavigableMap m=new TreeMap();
而NavigableMap是TreeMap的父接口,所以这里发生多态了。
这里可以理解m就代表TreeMap集合类的对象。
2)在TreeSet集合类中的add()函数体里面,使用对象m调用的put()函数肯定是TreeMap集合中的函数,所以接下来我们要去TreeMap集合类中找put()函数。
说明:
通过查看TreeMap集合中put函数中得知,在底层是要将我们自定义类型Student的对象强制转换为Comparable比较器接口对象,而在java中面向对象中。如果两种类型相互转化必须要有继承或者实现关系,而这里我们自定义的类Student和Comparable接口根本就没有关系,所以会发生类型转换异常。
知道错误原因之后,接下来我们要学习Comparable接口到底是什么?为什么底层要使用它。
2、Comparable接口介绍(掌握)
2.1 Comparable接口概述
通过查阅API得知:如果一个类要排序,那么这个类必须实现这个接口。
问题1:之前向TreeSet集合中存储数据为什么存储整数和字符串就没问题呢?
整数的包装类是Integer类型:
Integer实现了这个接口。
字符串的类型是String类型,而String类型也实现了这个接口。
由上述得知我们自己定义的类Student也要实现Comparable接口,这样在底层就可以强制类型转换了,就不会报异常了,既然不会报异常了就能调用Comparable接口中的compareTo()函数进行对Student的对象自然排序了。
既然已经实现了这个接口,那么必须实现接口中的抽象方法:
说明:
1)实现这个方法,必须返回一个int值,这个值可以是正数、负数、0代表当前对象大于、小于、等于指定的对象。
如果返回值是正数:表示当前对象就是调用此函数执行的对象大于指定的对象,就是作为参数的对象;
如果返回值是0 :表示当前对象就是调用此函数执行的对象等于指定的对象,就是作为参数的对象;
如果返回值是负数:表示当前对象就是调用此函数执行的对象小于指定的对象,就是作为参数的对象;
2)一旦我们的对象实现了Comparable接口,实现了compareTo方法,代表我们的对象就可以比较大小了。这种比较称为自然排序。
2.2 解决上述向TreeSet集合中存放自定义对象时异常问题
通过以上分析,我们得出如下结论:
我们要向TreeSet中存储自定义对象,这个自定义对象所属的类需要实现Comparable接口,并实现compareTo方法。
改进程序:让Student类实现Comparable接口,并重写compareTo方法。
由于最开始我们想按照存储的学生对象的年龄进行排序,所以我们应该在Student类中复写compareTo()函数时,在函数中书写比较两个学生对象年龄的代码,这个compareTo()函数是用来比较当前学生对象和指定的学生对象的大小,返回值可以是正数 、 0、负数。
所以在该函数中对两个学生对象的年龄进行相减,然后将相减后的结果返回给底层代码就可以了。
修改Student类中的代码如下:
说明:
复写Comparable接口中的compareTo()函数,这个函数是用来比较两个学生对象是否相等。
如果函数返回是0代表往TreeSet集合中存储的数据都是相等的数据;
如果返回正数或者负数说明集合中的数据都不想等,则会将所有数据都保存到TreeSet集合中。
而我们想要的是年龄相等的学生在集合中只保存一个对象,所以这里我们需要调用两个学生对象的age属性进行比较:
this.age表示调用函数对象的年龄,
但是上述代码不能使用o.age,因为Object类中根本就没有age属性,age是子类Student类中特有的属性,多态发生了想使用子类特有的属性或者行为,这里需要强制类型转换------》Student s= (Student)o ;
强制类型转换有风险,使用需谨慎 。所以使用instanceof进行判断。
问题升级:
要求在TreeSet集合中保存自定义Student类对象时,只有年龄和姓名都相同时保存一个对象,而如果只有年龄或者名字相同时是可以保存在集合中的,不算重复相同的数据。
所以这里需要继续改进Student类的函数,我们除了比较年龄,在年龄一样的情况下,还要比较姓名:
这里假设年龄相等,那么我们怎么比较两个人的姓名呢?
注意:这里是比较姓名,而姓名是字符串,比较两个字符串可以使用String类中的equals()函数和compareTo()函数,那么这里我们应该使用compareTo函数,因为我们要给底层返回int类型,而equals返回值类型是boolean类型,所以我们使用compareTo函数比较姓名是否相等。
代码如下:
总结:
TreeSet集合存放对象时需要注意的问题:
需要保证对象具备比较功能,也就是对象所属的类一定要实现Comparable接口,并且需要实现compareTo函数。在compareTo函数中书写具体的比较方式。
TreeSet集合怎么保证对象的唯一:
当给TreeSet集合中保存对象的时候,会调用对象的比较功能compareTo函数,只要这个函数返回的0,就认为2个对象相同,只保存其中的一个,另一个对象不会被保存到集合中,直接被删除。
3、Comparator比较器介绍(掌握)
3.1 Comparator自定义比较器概述
给TreeSet集合中保存对象的时候,由于TreeSet集合可以对其中保存的对象进行排序。要求保存的对象必须具备比较功能。因此要求给TreeSet集合中保存的对象所属的类必须实现Comparable接口。
需求:把字符串保存到TreeSet集合中,但是要求按照字符串的长度排序。
分析:String类本身已经具备了比较功能,它的比较功能是按照字符串中字符的字典顺序比较的(就是使用Comparable接口的重写compareTo方法进行排序,我们把这种排序方式叫做自然排序)。而现在我们希望按照字符串的长度进行比较。现在String类中的比较功能不适合当前程序的要求了。
另外由于String类是final修饰的,我们无法去继承这个类,就无法去复写String类中的compareTo方法,无法复写String类中的compareTo方法,就无法将compareTo方法改成比较字符串长度的方法。
上述的问题,不能使用继承解决。Java给我们提供的相应的解决方案:
当我们要给TreeSet集合中保存对象的时候,如果对象具备的比较功能不适合当前我们的需求时,我们可以给TreeSet集合自身传递一个适合我们需求的比较对象。然后让TreeSet集合按照我们传递进去这个对象对其中需要保存的数据进行比较。
而这个对象就是Comparator对象。
通过查阅API,我们得知创建TreeSet集合对象时,使用的构造函数中参数就可以接收Comparator对象:
什么是Comparator接口呢?
Comparator接口可以对集合中的元素进行排序,通过查看源码分析,当我们把这个Comparator接口的实现类对象丢给集合之后,那么给集合中存放的对象,不会再使用对象自身的比较功能进行大小的比较,而会使用我们传递的Comparator的实现类对象进行比较。
Comparator接口的函数如下所示:
说明:
1)在Comparator接口中的compare方法中接受2个参数o1和o2,这2个参数就是集合中需要比较大小的2个对象;
2)这个方法,专门比较两个元素的大小:
A.如果o1>o2,则返回 正整数;
B.如果o1==o2,则返回 零;
C.如果o1<o2,则返回 负整数;
需求:把字符串保存到TreeSet集合中,但是要求按照字符串的长度排序。
完成上述需求代码如下:
分析步骤:
1)创建一个自定义比较器类MyComparator并实现Comparator接口;
2)MyComparator类复写Comparator接口的compare函数,接收的参数都是Object类型的,所以需要强制转换为Student类型的对象s1和s2;
3)使用对象s1和s2分别调length()函数获得字符串的长度并相减,将结果赋值给一个整数变量temp;
4)使用判断结构进行判断如果temp==0,说明字符串长度相同则调用String类中的compareTo函数按照字典顺序进行比较并返回结果;
5)如果长度不同返回长度的差值;
自定义比较器代码如下所示:
package cn.xuexi.set;
import java.util.Comparator;
public class MyComparator implements Comparator{
public int compare(Object o1, Object o2) {
/*
* 这里发生多态了,那么我们先把Object类型强制转换为String类型
* 这样才能使用String类的对象调用length()函数进行长度比较
*/
if(!((o1 instanceof String) && (o2 instanceof String)))
{
//说明不是String类型
throw new RuntimeException("传递的必须是字符串");
}
//程序能够运行到此处说明两个对象肯定都是字符串
String s=(String)o1;
String s1=(String)o2;
//调用函数获得字符串长度相减
int temp=s.length()-s1.length();
//如果字符串长度相同,按字符串的字典顺序比较
if(temp==0)
{
//说明长度相同
return s.compareTo(s1);
}
else
{
//说明长度不同 返回长度的差值
return temp;
}
}
}
创建集合的测试类代码如下所示:
分析和步骤:
1)创建一个测试类TreeSetDemo1;
2)创建自定义比较器类MyComparator的对象myComparator;
3)使用new关键字调用TreeSet类的的构造函数比较器类MyComparator的对象myComparator作为参数传递来创建集合TreeSet类的对象ts;
4)使用集合对象向集合中添加字符串数据;
5)使用迭代器遍历集合,并输出;
package cn.xuexi.set;
import java.util.Iterator;
import java.util.TreeSet;
/*
* 把字符串保存到TreeSet集合中,但是要求按照字符串的长度排序。
*/
public class TreeSetDemo1 {
public static void main(String[] args) {
//创建比较器对象
MyComparator myComparator = new MyComparator();
//创建集合对象
TreeSet ts=new TreeSet(myComparator);
//向集合中添加字符串数据
ts.add("abc");
ts.add("jjj");
ts.add("AAAAA");
ts.add("hahahsg");
//遍历集合
for (Iterator it = ts.iterator(); it.hasNext();) {
//输出集合中的数据
System.out.println(it.next());
}
}
}
注意:以下代码是扩展内容,了解即可:
使用匿名内部类的时候要先导入Comparator接口的包,否则会报错。
使用匿名内部类的方式来传递比较器对象完成测试类中的代码如下所示:
package cn.xuexi.set;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
/*
* 把字符串保存到TreeSet集合中,但是要求按照字符串的长度排序。
*/
public class TreeSetDemo1 {
public static void main(String[] args)
{
/*//创建比较器对象
MyComparator myComparator = new MyComparator();
//创建集合对象
TreeSet ts=new TreeSet(myComparator);*/
//匿名对象传递参数
// TreeSet ts=new TreeSet(new MyComparator());
TreeSet ts=new TreeSet(
new Comparator()
{
public int compare(Object o1, Object o2) {
/*
* 这里发生多态了,那么我们先把Object类型强制转换为String类型
* 这样才能使用String类的对象调用length()函数进行长度比较
*/
if(!((o1 instanceof String) && (o2 instanceof String)))
{
//说明不是String类型
throw new RuntimeException("传递的必须是字符串");
}
//程序能够运行到此处说明两个对象肯定都是字符串
String s=(String)o1;
String s1=(String)o2;
//调用函数获得字符串长度相减
int temp=s.length()-s1.length();
//如果字符串长度相同,按字符串的字典顺序比较
if(temp==0)
{
//说明长度相同
return s.compareTo(s1);
}
else
{
//说明长度不同 返回长度的差值
return temp;
}
}
});
//向集合中添加字符串数据
ts.add("abc");
ts.add("jjj");
ts.add("AAAAA");
ts.add("hahahsg");
//遍历集合
for (Iterator it = ts.iterator(); it.hasNext();) {
//输出集合中的数据
System.out.println(it.next());
}
}
}
结论: 如果集合中的对象元素没有使用自然排序,这个时候我们就必须给一个比较器类Comparator的对象。
3.2 关于TreeSet集合使用的总结
当我们使用TreeSet的时候,需要对元素进行排序,那么会有两种排序方式:
1)自然排序(升序):
要存储的元素本身需要实现Comparable接口,实现compareTo方法,那么这个类就成为了一个可比较的类。
TreeSet就会替我们对元素进行自然排序。
2)比较器排序:
我们自定义类,实现Comparator接口,实现compare方法,这个类就成为了一个比较器类,专门比较大小。
然后,我们在创建TreeSet的时候,需要把自定义比较器对象作为TreeSet的构造函数的参数传递给TreeSet集合,最后TreeSet就会使用比较器对象对我们的元素进行排序。这个时候,元素即便没有实现自然排序,也没事。
注意:向TreeSet集合中保存null的时候,只能使用自定义比较器,不能使用自然排序比较器,否则会报空指针异常。
4、Collection下的接口和实现类总结
Collection:集合体系的顶层接口。
Collection集合(接口):
|----List集合(接口):存取元素有序、可以存储重复元素、可以存储null、有角标,可以精确控制集合中的每一个元素。
|-----ArrayList集合(类):
1)实现了List接口;
2)底层使用可变数组;
3)方法都是围绕着角标操作的;
4)查询遍历效率比较高,增删的效率比较低;
5)属于线程不安全的集合类;
|-----LinkedList集合(类):
1)实现了List接口;
2)底层使用链表结构;
3)方法都是围绕着头和尾来设计的;
3)查询遍历效率比较低,增删的效率比较高;
4)属于线程不安全的集合类;
|-----Vector集合(类):底层可变数组,什么都慢,但线程安全,已经被ArrayList集合取代。
|----Set集合(接口):存取元素无序(LinkedHashSet除外,因为底层有链表,存取有序)、不能存储重复元素、只能存储一个null、没有角标,只能通过迭代器遍历获取元素。该集合中的方法全部来自于Collection集合中。
|-----HashSet集合(类):
1)实现Set接口;
2)底层使用哈希表结构,保证对象唯一依赖于对象的hashCode 和 equals方法;
说明:使用对象的内容先调用hashCode ()函数生成哈希码值,然后结合数组长度计算数组下标,如果生成的下标对应的空间中已经有数据,这时需要使用对象调用equals()函数来判断两个对象的内容是否相同,如果不相同,就会在当前空间在画出一个空间来保存当前的对象。如果相同,直接舍去。
3)查询元素和添加不重复元素的效率比较快;
|-----LinkedHashSet集合(类):
1)HashSet集合类的子类,存取元素有序,元素唯一,没有自己的特有方法;
2)底层使用链表+哈希表结构;
补充:哈希表用来保存数据,保证数据唯一,不重复。链表用来记录数据的添加顺序,保证数据存取有序。
|-----TreeSet集合(类):
1)实现Set接口;
2)底层使用二叉树结构;
3)存储的元素会按照一定的规则进行排序;
4)只要向TreeSet集合中存储数据,数据所属的类要么实现Comparable接口或者创建TreeSet类的对象时传递Comparator。
Comparable:自然排序
它可以让一个对象自身具备比较功能,哪个对象需要具备比较功能,这个对象所属的类就需要实现Comparable接口,实现其中的compareTo方法。
Comparator:自定义比较器
它是单独的比较器,可以把这个对象单独丢给TreeSet集合,那么这时集合中的元素就可以按照当前指定的这个比较器进行比较大小。开发者如果需要使用比较器的时候,需要使用定义类来实现Comparator接口,同时实现其中的compare方法。
Java 中集合类的关系图,如下图所示:
5、泛型技术
5.1、泛型的引入
集合是一个容器,可以保存对象。集合中是可以保存任意类型的对象。
List list = new ArrayList();
list.add(“abc”); 保存的是字符串对象
list.add(123); 保存的是Integer对象
list.add(new Person()); 保存的是自定义Person对象
这些对象一旦保存到集合中之后,都会被提升成Object类型。当我们取出这些数据的时候,取出来的时候一定也是以Object类型给我们,所以取出的数据发生多态了。发生多态了,当我们要使用保存的对象的特有方法或者属性时,需要向下转型。而向下转型有风险,我们还得使用 instanceof关键字进行判断,如果是想要的数据类型才能够转换,不是不能强制类型转换,使用起来相对来说比较麻烦。
举例:
现在要使用String类的特有方法,需要把取出的obj向下转成String类型。
String s = (String)obj;
代码如下:
需求:查看集合中字符串数据的长度。
分析和步骤:
1)创建一个ArrayList的集合对象list;
2)使用list集合调用add()函数向集合中添加几个字符串数据和整数数据;
3)迭代集合分别取出集合中的数据,并查看集合中的字符串数据的长度;
package cn.xuexi.generic.demo;
import java.util.ArrayList;
import java.util.Iterator;
/*
* 泛型引入
*/
public class GenericDemo1 {
public static void main(String[] args) {
// 创建集合对象
ArrayList list = new ArrayList();
// 向集合中添加数据
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(true);
list.add(123);
//迭代集合
for (Iterator it = list.iterator(); it.hasNext();) {
Object obj = it.next();
/*
* 需求:想使用String类型的特有的函数查看字符串的长度
* Object obj = it.next();上述代码发生多态了,想使用String类中特有的函数必须得强制类型转换
* 可是由于集合可以存储任意类型的对象,而这里只是适合String类型的强制类型转换,其他数据类型会报classCastException类转换
* 异常,如果为了不报异常只能在转换前需要判断,这样做比较麻烦
* 由于这里的需求只是遍历集合后对于取出来的数据是String类型,查看他的长度,其他数据类型不管
* 我们能否想办法不让运行时报错呢,在存储的时候就告诉我,只要是String类型的可以存储,其他数据类型不让存储,这样做起来效率会高一些
*/
String s=(String)obj;
System.out.println(s+"长度是"+s.length());
}
}
}
上述的情况会发生ClassCastException异常。发生这个异常的原因是由于集合中保存了多种不同类型的对象,而在取出的时候没有进行类型的判断,直接使用了强转。
换句话也就是说我们存储的时候,任何类型都让我们存储。
然后我们取的时候,却报错,抛异常。非常不靠谱。你应该在我存的时候就告诉我:我只能存字符串,其他引用数据类型不能存储,那么这样我在取出数据的时候就不会犯错了。
假设我们在使用集合的时候,如果不让给集合中保存类型不同的对象,那么在取出的时候即使有向下转型,也不会发生异常。
我们回顾下以前学习的数组容器:
在前面学习数组的时候,我们知道数组这类容器在定义好之后,类型就已经确定,如果保存的数据类型不一致,编译直接报错。
代码举例如下所示:
数组是个容器,集合也是容器,数组可以在编译的时候就能检测数保存的数据类型有问题,如果我们在定义集合的时候,也让集合中的数据类型进行限定,然后在编译的时候,如果类型不匹配就不让编译通过, 那么运行的时候也就不会发生ClassCastException。
要做到在向集合中存储数据的时候限定集合中的数据类型,也就是说编译的时候会检测错误。java中从JDK1.5后提供了一个新的技术,可以解决这个问题:泛型技术。
5.2、泛型技术介绍
泛型的格式:
<具体的数据类型>
使用格式:
ArrayList<限定集合中的数据类型> list = new ArrayList<限定集合中的数据类型>();
说明:给集合加泛型,就是让集合中只能保存具体的某一种数据类型。
使用泛型改进以上程序中存在的问题:
说明:由于创建ArrayList集合的时候就已经限定集合中只能保存String类型的数据,所以编译的时候保存其他的数据类型就会报错,这样就达到了我们上述保存数据的目的了。
小结:
一、 泛型的好处?
1)解决了集合中存储数据的不安全性;
2)把运行时可能发生的异常,放在编译时作为编译错误处理了,避免了运行时的异常;
3)省略了代码中的强制类型转换的书写;
二、注意事项:
1)泛型只支持引用数据类型(类类型或接口类型等),泛型不支持基本数据类型:
2)泛型不支持数据类型以继承的形式存在,要求前后泛型的数据类型必须一致:
3)在jdk1.7之后,泛型也可以支持如下写法:
4)泛型兼容老版本,但是尽量避免,开发中不建议如下写法:
说明:上述写法是左边有泛型,右边没有泛型。
注意:现在的开发中,泛型已经成为编写代码的规范。
6.3、泛型技术的简单应用(课下多书写)
练习1:泛型存储自定义对象版本。
分析和步骤:
1)定义一个Student学生类,在这个类中定义两个属性name和age,并生成get和set方法;
2)创建一个测试类GenericDemo2类,在这个类中创建ArrayList<Student>类的对象list;
3)使用集合对象list调用add()函数向集合中添加几个自定义的Student的对象,并赋值;
4)迭代遍历集合;
以下是Student类:
package cn.xuexi.generic.demo;
/*
* 描述学生
*/
public class Student {
//属性
String name;
int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
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;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
以下是测试类GenericDemo2:
package cn.xuexi.generic.demo;
import java.util.ArrayList;
import java.util.Iterator;
/*
* 使用泛型存储自定义对象
*/
public class GenericDemo2 {
public static void main(String[] args) {
// 创建集合
ArrayList<Student> list = new ArrayList<Student>();
//向集合中添加自定义对象Student
list.add(new Student("班长",19));
list.add(new Student("班花",18));
list.add(new Student("班草",23));
list.add(new Student("副班长",19));
//遍历集合
for (Iterator<Student> it = list.iterator(); it.hasNext();) {
Student s = it.next();
System.out.println("我叫"+s.getName()+",我今年"+s.getAge());
}
}
}
练习2:使用泛型完成之前学习过的自定义比较器类Comparator。
需求:向TreeSet集合中存储String类型的字符串,按长度比较字符串大小,如果长度相同再按自然排序比较。
分析和步骤:
1)自定义一个比较器类MyComparator 实现Comparator<String>接口;
2)在比较器类MyComparator复写Comparator<String>接口中的compare()函数,在这个函数中按字符串长度比较字符串大小,如果长度相同,按自然排序比较;
3)定义一个测试类GenericDemo3 ;
4)在这个测试类中的main函数中创建TreeSet<String>集合对象set,自定义比较器对象new MyComparator()作为TreeSet<String>集合构造函数参数传递
5)使用set集合向TreeSet集合中添加几个字符串对象数据;
6)遍历集合;
以下是自定义比较器类MyComparator:
package cn.xuexi.generic.demo;
import java.util.Comparator;
/*
* 自定义比较器类
* 由于要向TreeSet集合中存储String类型的数据,所以这里比较的也必须是String类型的数据,
* 所以这里的泛型直接写String类型即可
*/
public class MyComparator implements Comparator<String>{
public int compare(String s1, String s2) {
// 按长度比较
int temp=s1.length()-s2.length();
if(temp==0)
{
//长度相同,按自然排序
return s1.compareTo(s2);
}
else
{
//长度不同 返回temp
return temp;
}
}
}
测试类GenericDemo3类:
package cn.xuexi.generic.demo;
import java.util.Iterator;
import java.util.TreeSet;
/*
* 向TreeSet集合中存储
*/
public class GenericDemo3 {
public static void main(String[] args) {
// 创建集合对象
TreeSet<String> set=new TreeSet<String>(new MyComparator());
//向集合中添加数据
set.add("AAAAAAA");
set.add("aaa");
set.add("aaaaaaa");
set.add("bb");
set.add("AAAAAAA");
set.add("hsgsh");
set.add("d");
//迭代集合
for (Iterator<String> it = set.iterator(); it.hasNext();) {
String s = it.next();
System.out.println(s);
}
}
}
补充:泛型的擦除技术:
泛型其实是一种编译器技术,泛型技术主要是在程序的编译的时候限定程序中的数据类型,一旦程序编译完成之后,生成的class文件中是没有泛型的。
因为既然使用泛型技术限制了数据类型之后并且编译通过了,到运行这里集合中的数据类型肯定已经统一了,没有必要在class文件中加上泛型了。