菜鸟学习笔记:Java容器3--泛型、排序
泛型
编成中我们往往会遇到这种情况,在接收参数时无法明确入参的类型,比如我们要接收一个学生的成绩,那么它可能是整数、也可能是小数,还有可能是字符串,早期的Java通常将不能确定的入参定义为Object类型来接收各种类型的参数。在获取时进行强制类型转换。但是这样会带来一个问题,把对象扔进集合中,集合是不知道元素的类型是什么的,仅仅知道是Object。因此在get()的时候,返回的是Object。外边获取该对象,还需要强制转换,如果会出现不能转换的情况会报出ClassCastException异常。泛型正好帮我们解决了这一问题。
泛型的作用就是把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型,说白了就是把参数的类型当作参数一起传递。这样在编译过程中就可以限定入参的类型。我们用例子来说明:
泛型类
使用时确定类型
注意:
1、泛型只能使用引用类型,不能基本类型
2、泛型声明时字母不能使用 静态属性|静态方法上
// 这里的T1和T2可以是任意合法名称在习惯上K V 分别代表键值中的Key Value。E 代表Element。
public class Student<T1,T2> {
private T1 javaScore;
private T2 oracleScore;
//泛型声明时不能使用 静态属性|静态方法上
//private static T1 test;
public T1 getJavaScore() {
return javaScore;
}
public void setJavaScore(T1 javaScore) {
this.javaScore = javaScore;
}
public T2 getOracleScore() {
return oracleScore;
}
public void setOracleScore(T2 oracleScore) {
this.oracleScore = oracleScore;
}
public static void main(String[] args) {
//使用时指定类型(引用类型),此时T1为String,T2为Intrger
Student<String,Integer> stu = new Student<String,Integer> ();
//1、安全:类型检查,这时如果你设置成非字符串类型在编译时就会报错
stu.setJavaScore("优秀");
//2、省心:类型转换,不用再像Object类型那样进行强转。
int it =stu.getOracleScore(); //自动拆箱
}
}
泛型接口
接口中 泛型字母只能使用在方法中,不能使用在全局常量中
public interface Comparator<T> {
T a;//会报错,因为全局常量是static final类型
void compare(T t);
}
泛型方法
泛型方法方法只能访问对象的信息,不能修改信息。
public static <T> void test(T a){//这里的a无论是什么类型只能访问,不能修改
System.out.println(a);
}
public static void main(String[] args) {
test("a"); //T -->String
}
最后需要注意的是泛型没有多态的概念,如果直接使用
A a = new A();会报错。并且规定不能使用instanceof来判断是否为泛型实例。
泛型继承
父类的泛型可以被子类所继承,也可以被子类所擦除,这个特点在普通类、抽象类以及接口中均适用,下面我们通过例子来说明:
假设我们在父类中定义了如下的泛型:
public abstract class Father<T,T1> {
T name;
public abstract void test(T t);
}
子类在继承时可以直接指定泛型的具体类:
class Child1 extends Father<String,Integer>{
//此时name自动变为String类型
@Override
public void test(String t) {//重写自动变为String t
}
}
如果子类是泛型类,父类也指定为泛型,那么泛型结构必须和父类一致或大于父类:
//子类的泛型类必须有T1和T,T1和T顺序可以调换。
class Child2<T1,T,T3> extends Father<T,T1>{
T1 t2;
@Override
public void test(T t) {
}
}
如果不指定出父类的泛型,那么该泛型就会变成Object类型,这就是泛型的擦除。所以上例也可以在子类中指定泛型类,父类中不指定,这样泛型就会被擦除,使用Object替换。
class Child3<T1,T2> extends Father{
T1 name2;
@Override
public void test(Object t) {
// TODO Auto-generated method stub
}
}
也可以子类和父类同时擦除泛型,所有泛型都用Object来替换:
class Child4 extends Father{
String name;
@Override
public void test(Object t) {
}
}
但需要注意子类擦除而父类包括泛型的情况是不允许的。
在泛型擦除时需要注意被擦除的泛型相当于Object但不完全等同于Object。比方说:
public static void test(Student<Integer> a){
}
public static void test1(Student<?> a){//?表示泛型不定
}
public static void main(String[] args) {
Student stu1 = new Student();
Student<Object> stu = new Student<Object>();
test(stu1); //stu1 相当于Object 但是不完全等同Object
//test(stu);//会报错,说明类型擦除后,编译时不会类型检查
test1(stu1);
test1(stu);
}
通配符"?"
"?"用在泛型中表示可以接收泛型的任意类型,只能接收和输出,不能进行修改。一般与extends和super关键字搭配使用。
? extends 泛型上线 表示问号可以接受的泛型类必须是泛型上线的子类(可以包括上线)。
? super 泛型下线 表示问号可以接受的泛型必须是泛型下线的父类(可以包括下线)。
下面通过举例来说明:
首先定义一个Fruit类和Apple类:
public class Fruit {
}
class Apple extends Fruit{
}
在定义一个Student类用于测试:
public class Student<T> {
T score;
//可接收任意类型
public static void test(Student<?> stu){
}
//Fruit类的子类
public static void test2(Student<? extends Fruit> stu){
}
//指定类型
public static void test3(Student<Fruit> stu){
}
//Fruit类的父类
public static void test4(Student<? super Fruit> stu){
}
public static void main(String[] args) {
//可以?类的泛型指向具体类的对象
Student<?> stu = new Student<String>();
test(new Student<Integer>());
//Apple为Fruit子类
test2(new Student<Apple>());
//test3(new Student<Apple>()); //泛型没有多态,非Fruit类都会报错
//test4(new Student<Apple>()); //会报错,Apple不是Fruit的父类
stu = new Student<Fruit>();
//test4(stu); //stu为Student<?>类型引用变量,与test4接收变量的类型不符,会报错
test4(new Student<Object>());//Object是Fruit父类,合法
test4(new Student<Fruit>());//extends可以包括自己
}
}
泛型知识点补充
- 在泛型中还可以使用泛型做嵌套:
public class test <T>{
T stu ;
public static void main(String[] args) {
//泛型的嵌套
test<Student<String>> room =new test<Student<String>>();
//从外到内拆分
room.stu = new Student<String>();
Student<String> stu = room.stu;
String score =stu.score;
}
}
没有泛型数组,比如:
test<String>[] arr2 = new test<String>[10];
//test<String>[] arr2可以声明,但不能创建new test<String>[10]
容器排序
Comparable接口与compareTo方法
在可以进行排序的实体类中都实现了java.lang.Comparable接口,此接口中只有一个compareTo方法,该方法通过返回值的正负对this和传入对象作比较,返回0表示两对象相等,返回正数表示this>obj,返回负数表示this<obj,实现了Comparable接口的类通过实现compareTo方法从而确定该类对象的排序方式。
基本类型包装类的比较比较简单,整数和小数以及日期类比较其基本数据类型值的大小,字符比较字符的ASCII码的大小,一般a-z递减,大写字母大于小写字母。
String类的compareTo方法从两个字符串的第一个字符开始进行比较,相等就比较下一个字符,直到一个字符串走完为止,如果走完则比较字符串长度,相等则相等,否则长度大的字符串大,其源码如下:
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
//取
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
有了compareTo方法,我们就可以对任意类型数组进行排序
public static <T extends Comparable<T>> void sort(T[] arr){
boolean sorted = true;
//需进行length-1次冒泡
for(int i=0;i<arr.length-1;i++){
//该变量用于优化冒泡排序,一次比较中如果未发生交换则排序结束
sorted = true;
for(int j=0;j<arr.length-1-i;j++){
//用Comparable方法判断数组内容大小
if(((Comparable)arr[j]).compareTo(arr[j+1])>0){
T temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
sorted = false;
}
}
if(sorted){
break;
}
}
}
Comparator接口与compare方法
与Comparable接口功能相同,java.util.Comparator接口通过重写compare方法也可以实现排序,下面举例说明,这次我们按照字符串的长度来进行排序:
首先来定义排序规则类(也可以不定义这个直接使用匿名内部类):
public class StringComp implements java.util.Comparator<String>{
//按字符串长度排序
@Override
public int compare(String o1, String o2) {
int len1 =o1.length();
int len2 =o2.length();
return -(len1-len2);
}
}
然后定义一个工具类Utils定义排序方法,接收一个排序规则:
public class Utils{
public static <T> void sort(Object[] arr,Comparator<T> com){
//从大到小排序 降序
boolean sorted= true;
int len =arr.length;
for(int j=0;j<len-1;j++){ //趟数
sorted =true; //假定有序
for(int i=0;i<len-1-j;i++){ //次数
if(com.compare((T)arr[i], (T)arr[i+1])<0){
Object temp = arr[i];
arr[i] =arr[i+1];
arr[i+1] =temp;
sorted =false; //假定失败
}
}
if(sorted){ //减少趟数
break;
}
}
}
}
这样我们就可以在调用时通过传入我们的排序规则对对象进行排序:
public static void main(String[] args) {
arr2 =new String[]{"a","abcd","abc","def"};
Utils.sort(arr2,new StringComp());
System.out.println(Arrays.toString(arr2));
}
这样就实现了一个简单的比较类,当然实际工作中不需要我们自己写排序规则Utils,在Java为我们提供了Collections类已经很好的帮我们实现了各种有序容器的排序,使用方法如下:
public static void main(String[] args) {
List<String> list =new ArrayList<String>();
list.add("a");
list.add("abcd");
list.add("abc");
list.add("def");
//匿名内部类方法实现
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
int len1 =o1.length();
int len2 =o2.length();
return -(len1-len2);
}
});
System.out.println(list);
}
TreeSet和TreeMap(了解)
Set和Map在是无序不可重复的容器,但Java中也提供了可以对其进行排序的实现类,那就是TreeSet和TreeMap。
TreeSet通过判断compareTo方法或者compare方法的结果是否为0来判断两元素是否相等,所以不需要重写hashcode和equals方法。举例说明:
先定义一个实体类:
public class Person implements Comparable<Person> {
private String name;
private int age;
...//get和set方法
public int compareTo(Person o) {
return 0; //当compareTo方法返回0的时候集合中只有一个元素
return 1; //当compareTo方法返回正数的时候集合会怎么存就怎么取
return -1; //当compareTo方法返回负数的时候集合会倒序存储
}
}
public static void main(String[] args) {
TreeSet<Person> ts = new TreeSet<>(new Comparator<String> {
@Override
//其实用compareTo或compare一个方法就可以实现功能,这里综合一下两个比较器的用法
public int compare(String s1, String s2) {
int num = s1.length() - s2.length();
return num == 0 ? s1.compareTo(s2) : num;
}
});
Person p1 = new Person("张三", 11);
Person p2 = new Person("李四", 12)
Person p3 = new Person("王五", 15)
Person p4 = new Person("赵六", 21)
ts.add(p1);
ts.add(p2);
ts.add(p3);
ts.add(p4);
p1.setAge(13);
}
注意TreeSet容器只会对添加的数据排序,如果对元素进行修改中不会影响它的排序位置。比如上例中最后添加p1.setAge(13);不会改变排序结果,张三还是会排在第一位。同样set方法设置后容器还可能出现重复的情况。TreeMap的用法以及带来的问题与TreeSet相似,这里不再赘述。
上一篇:菜鸟学习笔记:Java提升篇2(容器2——Map、Set、迭代器)
下一篇:菜鸟学习笔记:Java提升篇4(容器4——Collections工具类、其他容器)