泛型
泛型
1.泛型
1.1.泛型的引入
需求:打印ArrarList集合中所有字符串的长度;
解决问题的代码:
public class Demo1 {
public static void main(String[] args) {
//需求:打印ArrarList集合中所有字符串的长度;
//新功能,需要定义函数实现
ArrayList list = new ArrayList();
list.add("1");
list.add("22");
list.add("333");
//调用函数输出结果
print(list);
}
/*
* 功能:打印ArrayList集合中的所有字符串的长度
* 两个明确:
* 1、返回值类型: 因为只需要打印输出所有字符串的长度,不需要返回什么结果,所以返回值类型是void
* 2、参数列表:ArrayList list
* */
public static void print(ArrayList list){
//要输出所有字符串的长度,所以需要遍历整个集合
for (Object object : list) {
//因为要当作字符串操作,所以要强制向下转型
String str = (String)object;
System.out.println(str.length());//输出字符串的长度
}
}
}
1、在添加数据的时候人为的注意只能保存字符串;问题是:人为的注意,总有疏忽的时候;
2、在强制类型转换之前先做判断,判断这个对象确实是String类型;
问题是:不符合面向对象的一个原则:谁的工作谁负责做;
自从JIK1.5之后,出现一个新的技术,叫做泛型,就可以解决这个问题;
1.2.泛型介绍
泛型,又叫做参数化类型,就是一种在编译期对数据类型(只能是引用数据类型)进行检查的技术;
泛型的写法是:
<数据类型>
这个字母E,就是一个标识符,表示一个具体类型的参数;
使用泛型解决刚才的问题:
使用泛型的时候需要注意:
泛型只在编译期起作用;编译后的字节码里面没有泛型;
泛型只存在编译期,编译后泛型消失,叫做泛型的擦除;
1.3.泛型的简单应用
需求:向集合中添加学生对象,要求按照学生的年龄进行从小到大排序
public class GenericTest {
public static void main(String[] args) {
//需求:向集合中添加学生对象,要求按照学生的年龄进行从小到大排序
//因为没有映射关系,有要求保存后排序,所以可以使用TreeSet集合
//TreeSet排序,必须有比较的功能,所以要么让Student实现Comparable接口,要么提供一个比较器对象
//这里使用比较器对象
//因为这里只使用一次,所以可以使用匿名内部类的方式;
Comparator<Student> c = new Comparator<Student>(){
public int compare(Student o1, Student o2) {
if(o1 == null || o2 == null){
throw new NullPointerException("保存的数据不能是null");
}
return o1.age - o2.age;
}
};
TreeSet<Student> ts = new TreeSet<>(c);
ts.add(new Student("小明",23));
ts.add(new Student("小张",21));
ts.add(new Student("小王",22));
ts.add(new Student("王五",22));
ts.add(new Student("小李",25));
for (Student object : ts) {
System.out.println(object);
}
}
}
//描述学生对象
class Student{
String name;//姓名
int age;//年龄
public Student(String name, int age) {
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;
}
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
结论:使用泛型,可以在编译期就对数据的类型进行强制要求,可以提高程序的安全性;
同时避免使用强制类型转换,减小出错的几率;
为了提高程序的安全性,一般使用集合时都不会保存多种类型的数据,而是使用泛型限制只保存同一种类型的数据;
1.4.泛型类
使用泛型时,泛型可以直接定义在类上;如果在定义一个类时使用了泛型,那么这个类就叫做泛型类,泛型的具体类型在创建对象时确定;
需求:定义一个工具类,可以保存和获取一个任意类型的对象;
使用泛型类需要注意:
1、静态函数不能使用类的泛型
2、如果一个类要定义多个泛型,都写在同一个尖括号中,不同的泛型之间使用逗号隔开;
1.5.泛型方法
如果方法要接受的参数不确定,而且和类上的泛型不一致,就可以在方法上自己定义泛型;
案例:修改上面的泛型工具类,提供一个功能,可以接收任何引用类型的参数;
在一个函数中,参数列表中可以混合使用类的泛型和方法的泛型;
方法的泛型实在调用方法的时候传参的时候确定的,调用方法传参和有没有对象无关,所以静态函数一样可以定义方法的泛型;
方法上的泛型是定义在方法上的,属于局部范围的,所以:
注意:在方法中定义的泛型,只能在这个方法里面使用;
1.6.泛型接口
如果在定义一个接口时在接口上使用的泛型,这个接口就成为泛型接口;
1、接口的泛型,在实现接口时可以确定;
2、在一个类实现一个泛型接口的时候,可以不指定泛型的具体类型;
像这样,在实现接口或者继承类的时候,把泛型也继承下来,叫做泛型的传递;
1.7.泛型通配符
1.7.1.泛型中的通配符
需求:定义功能,输出不同集合中的不同元素;
以后要使用泛型,需要适配各种不同的类型的时候,就应该使用泛型的通配符;
public class GenericDemo2 {
public static void main(String[] args) {
//需求:定义功能,输出不同集合中的不同元素;
/*
* 1、简单点,简单的功能,输出List集合中的元素,元素类型是String类型
* */
List<String> list = new ArrayList<>();
list.add("11");
list.add("22");
printList(list);
//2、提升功能,使这个函数可以输出Set集合,集合中的元素也是String类型
Set<String> set = new HashSet<>();
set.add("aaa");
set.add("bbb");
printList(set);
Set<Integer> set2 = new HashSet<>();
set2.add(123);
set2.add(456);
printList(set2);
}
//要让函数既能接收List类型的集合,也能接收Set类型的集合,所以要将参数的类型提升为他们共同的父类型
public static void printList(Collection<?>/*?表示泛型的通配符*/ list){
for (Object string : list) {
System.out.println(string);
}
}
}
1.7.2.通配符的上限和下限
通配符的上限,表示只能匹配某一个类或者它的子类的类型;写法是:<? extends 上限类的类型>;
通配符的下限,表示只能匹配某一个类或者它的父类的类型;写法是:<? super 下限类的类型>
在使用通配符的上限和下限时,如果是在集合容器上使用,应该注意:
1、如果容器使用的是上限,此时只能从集合中取出数据,不能向集合中存放数据;
public class GenericDemo4 {
public static void main(String[] args) {
List<Z> list1 = new ArrayList<>();
list1.add(new Z());
list1.add(new Z());
test1(list1);
List<S> list2 = new ArrayList<>();
list2.add(new S());
list2.add(new S());
test1(list2);
}
//表示接收的参数是一个list集合,集合中只能保存Z类或者Z类的子类
public static void test1(List<? extends Z> list){
/*
* 这个函数接收的参数是一个List集合对象,实际接收到的对象,可能只能保存Z类,
* 可能只能保存S类,或者它们还有其他子类,只能保存某一个子类;
* 如果实际接收的对象,只能保存S类,此时向容器中添加Z类的对象,就添不进去,会报错;
* 为了避免出现这种情况,所以就不允许向使用泛型的上限的集合中添加数据;
* */
// list.add(new Z());
for (int i = 0; i < list.size(); i++) {
/*
* 因为实际接收到的集合对象中保存的数据,类型要么是Z类,要么是Z的子类,
* 都可以使用Z类指代
* */
Z z = list.get(i);
System.out.println(z);
}
}
}
class F{}
class Z extends F{}
class S extends Z{}
2、如果容器使用的是下限,可以向容器中添加数据,添加的数据的类型只能是下限的类型或者它的子类的对象;如果要从里面取出数据,必须使用强制类型转换,或者使用Object类型接收;
1.8.泛型总结
1、泛型是一种在编译期就可以进行数据类型的检查的技术,只能检查引用类型的数据类型;
2、泛型使用一对尖括号表示:<标识符>;尖括号中的标识符,表示一个引用数据类型;
3、泛型可以定义在类、方法和接口上;
a)定义在类上:书写在类名后面,在创建类的实例对象时确定泛型的具体类型;在类中的非静态函数中可以使用;
b)定义在方法上:书写在方法的返回值类型前面,在方法调用时确定泛型的具体类型,静态和非静态函数都能定义;只能在当前定义的这个方法里使用;
c)定义在接口上:书写在接口名后面,实现接口时可以明确泛型的具体类型,或者通过泛型类,在创建类的对象时才明确具体类型(这个叫做泛型的传递);
4、泛型的通配符:当需要使用的泛型的数据类型是不确定的时候,就需要使用泛型的通配符;
通配符的写法:<?>
5、泛型的通配符的上限和下限:
a)在定义泛型的通配符的时候,如果只能匹配某个类和这个类的子类,就需要使用通配符的上限,写法是:<? extends 上限的类型>
在使用通配符的上限时,只能从容器中取出数据,不能向容器中存放数据;
b)在定义泛型的通配符的时候,如果只能匹配某个类和这个类的父类,就需要使用通配符的下限,写法是:<? super 下限的类型>
在使用通配符的下限的时候,直接向容器中保存对象,对象的类型只能是下限的类型或者下限类型的子类型;
要从容器中取出对象,不能直接使用下限类型的变量接受,需要强制向下转型,或使用Object类型的变量接受;
上一篇: LeetCode 785. Is Graph Bipartite?
下一篇: 泛型