2019/7/12 14:15二刷留念
01-集合框架(TreeSet)
接下来说Set集合中的TreeSet。
|——Set:元素是无序(存入和取出的顺序不一定一致),元素不可以重复。(想重复找List,不重复找Set)
|——HashSet:底层数据结构是哈希表。
HashSet是如何保证元素唯一性的呢?
是通过元素的两个方法,hashCode和equals来完成。
如果元素的HashCode值相同,才会判断equals是否为true。
如果元素的hashcode值不同,不会调用equals。
|——TreeSet:可以对Set集合中的元素进行排序。
Set集合是无序的,这点有点缺憾,于是TreeSet就来弥补这个问题。
先写一下试试:
运行:
我们发现,虽然它打印的顺序和我们存的顺序不一样,但是也是有自己的固定顺序的。
02-集合框架(TreeSet存储自定义对象)
需求:
往TreeSet集合中存储自定义对象学生。
想按照学生的年龄进行排序。
Student类:
主函数:
但是运行的时候发现报错了:
我们试着改一下,只存入一个元素:
运行:
发现成功了。我们再存两个试试:
发现又挂掉了:
这是为什么呢?
我们找到java.lang包里的Comparable接口。
到这里我们就大概明白了。
TreeSet集合可以实现排序,可是按照什么方式排,我们并没有告诉它。而这个例子中的学生类,根本不具备比较性。而我们必须要让元素具备比较性,TreeSet集合才能帮我们排序。这个时候我们找到了Comparable接口,只要实现这个接口,学生类就具备了比较性。
这个接口中只有一个方法:
点进去看看:
其中指定对象就是传进来的o,而此对象是调用这个方法的this。
让学生类实现Comparable接口,并重写compareTo方法:
运行:
我们再输出一下它的比较过程,看看都和谁比过:
我们发现,CompareTo方法会自动被调用,底层在调用这个方法,当然这个类首先得实现Comparable接口。符合我们的规则,当然就可以调用子类实现的方法。
排序成功了,可是还存在一个小问题,当出现同年龄而不同名的元素时:
运行:
我们发现,lisi08没有被存进来。
为什么呢?
我们看一下重写的compareTo方法,它是这么写的:
当两者年龄相同时,返回0。
再看看compareTo方法的详细信息:
返回0意味着此对象等于指定对象,也就是说这两个对象相当于相同对象。
这就是lisi08没有存进来的原因,程序将它们识别为相同对象了。
我们的修改思路是,若年龄相同,则继续判断姓名是否相同。
String类中有个compareTo方法:
String类它本身实现了Comparable,Java中很多类都具备比较性:
我们调用一下这个方法(因为都是实现了Comparable接口,所以返回的也是正数、负数或0):
运行:
记住,排序时,当主要条件相同时,一定要判断一下次要条件。
03-集合框架(二叉树)
那么TreeSet底层的数据结构是怎样的呢?
排序无非就是比较,元素越多,有可能互相比较的次数越多,这个时候效率就没那么高了。为了优化这个底层,TreeSet用了一个比较特殊的数据结构。
用一个例子画了张二叉树的图,图上标的数字是取出数据的顺序:
下面这个例子中:
我们发现它存进和取出的顺序是一样的。
下面画一下存取的过程,存放过程:
而取出是从左向右来取,所以取出顺序为22、20、19、19。正序取出。
我们如果返回-1:
存放过程:
取出顺序为19、19、20、22。倒序取出。
如果返回0:
发现里面只存进了一个元素:
现在补充完整这张体系图:
|——Set:元素是无序(存入和取出的顺序不一定一致),元素不可以重复。(想重复找List,不重复找Set)
|——HashSet:底层数据结构是哈希表。
HashSet是如何保证元素唯一性的呢?
是通过元素的两个方法,hashCode和equals来完成。
如果元素的HashCode值相同,才会判断equals是否为true。
如果元素的hashcode值不同,不会调用equals。
|——TreeSet:可以对Set集合中的元素进行排序。
底层数据结构是二叉树。
保证元素唯一性的依据:compareTo方法return 0。
TreeSet排序的第一种方式:让元素自身具备比较性。 元素需要实现Comparable接口,覆盖compareTo方法。 这种方式也称为元素的自然顺序,或者叫做默认顺序。
TreeSet的第二种排序方式: 当元素自身不具备比较性时,或者具备的比较性不是所需要的, 这时就需要让集合自身具备比较性。 集合初始化时,就有了比较方式。
04-集合框架(实现Comparator方式排序)
接下来讲TreeSet的第二种排序方式:
TreeSet的第二种排序方式: 当元素自身不具备比较性时,或者具备的比较性不是所需要的, 这时就需要让集合自身具备比较性。 集合初始化时,就有了比较方式。
集合对象以初始化就有的比较方式,就要看一下构造函数了。
为了让集合自身具备比较性,就定义了比较器,将比较器对象作为参数传递给TreeSet集合的构造函数。
我们依然在上节课代码的基础上做出修改。
上节课是按照年龄排序,现在我们的需求变了,想按照姓名来排序。
我们需要使用的这个构造函数要用到这个东西:
点进去看看:
发现是一个接口,这个接口有两个方法:
它的返回值:
下面定义一个新的类实现Comparator接口:
在定义的容器的时候将这个新的类的对象作为参数传进去:
运行:
成功了哦。
接下来解决姓名相同年龄不一样的情况:
问题解决了呢。
我们还有更简单的方式来写。年龄是一个整数,而整数有自己的对象:Integer。
我们去Integer类中看一下,发现它也实现了Comparable接口:
所以它也有compareTo方法:
所以我们这样写就可以了:
运行:
都是OK的。
有两种排序方式:让元素自身具备比较性,让容器自身具备比较性。
当两种排序都存在时,以比较器为主。(比如上面例子中,最后就是以比较器(姓名)为序排序的)
定义比较器:定义一个类,实现Comparator接口,覆盖compare方法。
这两种排序方式中,比较器比较常用一些。
比较器是什么概念呢?
叫做你具备了比较性,我就按照你的排,如果你没有比较性/或者比较性不是所需要的,我对外提供了一个规则,你只要按照这个规则来写,我依然可以帮你排。
这个规则就是Comparator。
接口就是对外提供的功能扩展。
05-集合框架(TreeSet练习)
练习:按照字符串长度排序。
我们先运行一下这段代码:
发现它是按照自然顺序来排的。
字符串本身具备比较性,但是它的比较方式不是所需要的,这时就只能使用比较器。
我们修改一下代码:
运行:
发现排序成功。
我们还可以让代码更简单一点,依然是上节课的那个方法:
运行:
发现“aaa”没有存进去。
因为它和“cba”长度一样。
我们继续修改代码,主要条件判断完了,要判断次要条件:
再运行:
OK了。
当然,我们也可以用匿名类的方式来完成:
但是这样阅读性就差一些,所以还是用正常方式写好一点。
06-集合框架(泛型概述)
接下来讲下一个知识点。
先看示例:
我们集合当中会添加很多对象,比如说添加一个Integer对象:
集合当中只能添加对象,是不能添加基本数据类型的,但是在1.5版本之后可以添加基本数据类型了,因为基本数据类型有一个自动装箱拆箱动作,所以我们直接这样写就行,自动把4封装成对象了,两句话意思一样,:
我们试着编译运行一下,发现编译并没有问题,可是运行的时候就挂掉了:
这个异常是类型转换异常,说Integer不能转换成String。
可是编译的时候都没有发现,运行时却挂掉了。我们有没有办法让它在编译时就可以发现问题呢?
我们分析一下问题怎么产生的:存入了不同类型的数据。
那么,如果往里面全存成String,是不是就可以了呢?
Java在1.5版本的时候就对这个问题进行了解决,提供了一个新技术,叫做泛型。
泛型:JDK15版本以后出现的新特性,用于解决安全问题,是一个类型安全机制。
再复习一下新特性产生的三种原因:1,高效。2,简化书写。3,提高安全性。
那么它如何解决安全性问题呢?
我们这里可以借鉴数组定义的原则来完成。
我们看这个示例:
编译的时候就出错了:
为什么呢?
因为在定义数组容器的时候已经明确类型了,已经确定是int了。
这也是数组比较安全的原因。
而集合在定义的时候并未指定元素类型,所以存在了安全隐患。
如果能像数组一样在集合定义时就指定类型,是不是就没有安全隐患了呢?这就是泛型的由来。
我们现在为集合指定数据类型:
这句话的意思是:我定义了一个容器,叫做ArrayList容器,这个容器中的元素是String类型。
为什么使用的是尖括号<>呢?因为大括号{}已经被程序使用了,小括号()被参数使用了,方括号被[]数组使用了,就只剩尖括号<>了(;′⌒`)
好了,现在编译,发现失败了,问题从运行时期就转移到了编译时期:
好,我们把那条错误语句去掉,然后再做一些小修改,将迭代器也指定数据类型:
编译,发现错误提示没有了:
而且,不仅错误提示没有了,之前编译会有的这两句注意事项也没有了:
为什么没有了呢?
因为安全了呀。
泛型的好处:
1,将运行时期出现的问题ClassCastException,转移到了编译时期,方便于程序员解决问题,让运行时的问题减少,提高了安全性。
2,避免了强制转换的麻烦。
07-集合框架(泛型使用)
泛型格式:通过<>来定义要操作的引用数据类型。
那么在使用Java提供的对象时,什么时候写泛型呢?
通常在集合框架中很常见,只要见到<>就要定义泛型。
我们看一下Collection接口:
他就有<E>,在这里,E是Element,即元素的意思,它并没有什么具体的含义。
我们再看一下他的子类ArrayList:
它里面也有E。
我们再看看方法列表:
比如add方法,我们明确的是什么类型的元素,那么add方法添加的就是什么类型的对象。
其实<>就是用来接收类型的。
当使用集合时,将集合中要存储的数据类型作为参数传递到<>中即可。(就如同我们的函数传参数一样)
接下来写我们依然写之前写过的那个例子,想将存进集合中的字符串按长度来排序。
我们发现Comparator接口也可以指定数据类型:
它也可以加泛型,因为泛型避免了强转。
像我们之前写就需要强转:
而现在为它指定了数据类型,就不用强转了。
整个方法的代码就非常简单了:
编译运行:
执行很顺利,而且不再有安全提示了。
我们在写HashSet集合的时候,覆盖了两个方法,一个叫做hashCode,一个叫做equals,equals中必须要写Object。因为这个equals复写了Object,Object有泛型吗?没有。所以必须得做转换,转换之前还得要判断它是不是这种类型。
再总结一下,我们要定义一个对象需要做的事情:里面要自定义hashCode和equals方法,同时还要实现comparable接口,重写compareTo方法,让对象具备默认的比较性,这样既可以存到HashSet当中,又可以存到TreeSet当中。(不一定存储到哪个里面去,所以这些基本的特性都要具备)
08-集合框架(泛型类)
接下来有一个问题,我们能不能在定义的类中使用泛型的概念,来完成程序的设计呢?
换句话说,Java能定义出一堆泛型类,我们能不能定义出呢?
接下来体现一下泛型出现前后的代码。
这里有一个对应Worker类的工具类,接下来我们想定义一个Student类,也要给它定义一个工具类。那么,每个类都对应一个工具类,是不是太麻烦了呢?
如果只是为了设置对象和获取对象,对象类型不确定,都是后面有的。
这时,我们是不是可以提高一下程序的扩展性,抽取这些对象的共性类型。
所以换一种定义方式:
这样是否就通用了呢?
试一下:
编译运行发现都没有问题:
我们将代码修改一下:
这时再运行就出现了类型转换异常的问题:
其实,我们这里所写的Tool类,以前程序设计的时候都用这样的方式来完成程序的扩展,比如最常见的之一equals,它就是Object类型的。
这就是早期的代码,程序员只能自己弄清楚到底是什么类型,不能用什么类型,是很手工的方法。
接下来是泛型出现后的代码:
这就是传说中的泛型类。
主函数中:
编译运行都没有问题:
把最后一句的强转去掉:
发现编译运行也是OK的。(图略)
传进Student对象试一下:
编译时就会报错,直接让问题发生在了编译时期:
什么时候定义泛型类?
当类中要操作的引用数据类型不确定的时候,早期定义Object来完成扩展,现在定义泛型来完成扩展。
注意:这里是引用数据类型哦,基本数据类型定义不了。
09-集合框架(泛型方法)
泛型除了可以定义在类中,也可以定义在方法中。
例:
给类定义泛型后,为我们提供了很大方便,可是也出现了局限性,那就是对象一旦建立,类型就确定了。比如在这个例子中,只能打印固定类型的数据。
泛型类定义的泛型,在整个类中有效,如果被方法使用,那么泛型类的对象明确要操作的具体类型后,所有要操作的类型就已经固定了。为了让不同方法可以操作不同类型,而且类型还不确定,那么可以将泛型定义在方法上。
那我们需要建立多个对象,指定不同的数据类型。
这样就太麻烦了。
如果我们想实现的是,建立一个对象后,可以通过这一个对象打印多种类型数据。
修改后的代码:
不同类型的元素就打印出来了:
10-集合框架(静态方法泛型)
那我们可以在泛型类中定义泛型方法吗?
可以的。
比如这样:
使用它:
现在我们定义了一个静态方法:
编译会出现错误提示:
因为静态方法在对象没有建立的时候就存在了,而元素类型T只有在对象建立之后才会指定,所以出现问题了。
特殊之处:
静态方法不可以访问类上定义的泛型,如果静态方法操作的引用数据类型不确定,可以将泛型定义在方法上。
OK,我们修改一下这个静态方法:
再使用它运行就成功了:
注意不可以写在这里哦:
会编译失败:
这叫书写格式错误,泛型定义在方法上时,永远要放在返回值类型的前面,修饰符的后面。
11-集合框架(泛型接口)
泛型定义在接口Inter上,InterImpl类在实现这个接口的时候指定了数据类型:
运行,没有问题:
接下来是另一种情况,InterImpl类在实现这个接口的时候也不知道该操作什么数据类型(这种用法其实不多见):
运行,也是OK的: