泛型的说明以及使用

一、泛型

(一)泛型的概述和使用

1、泛型:广泛的类型。在定义类的时候,某些方法的参数列表或者返回值类型不确定,就使用一个符号,来表示那些尚未确定的类型,这个符号就是泛型。

2、使用:对于具有泛型的类型,在类名的后面加上尖括号,尖括号里面写上泛型的确定类型(在使用某个类型创建对象的时候,对象的泛型确定为什么类型,泛型就是什么类型)

3、泛型的好处:
(1)提高了数据的安全性,将运行期的问题提前暴露在了编译期
(2)避免了强转的麻烦

4、注意事项:
(1)泛型必须书写成引用数据类型,不能写基本数据类型
(2)泛型书写的时候,必须保持前后一致
(3)泛型的推断:如果【=】左侧的泛型已经写好,【=】右侧的泛型可以不写,会自动根据左侧的泛型确定为一样的类型。右侧直接写为【<>】,像一个菱形,所以也叫菱形泛型。JDK7的新特性

代码示例

import java.util.ArrayList;
import java.util.Iterator;

public class Demo01_Use {

public static void main(String[] args) {
	//一个集合的泛型确定为什么类型,就只能存储什么类型的数据
	//泛型必须写成引用数据类型
	ArrayList<String> list = new ArrayList<>();
	
	list.add("abc");
	list.add("def");
	list.add("ghi");
	list.add("jkl");
	//list.add(123);
	list.add("mno");
	list.add("pqr");
	//list.add(false);
	
	Iterator<String> it = list.iterator();
	
	while(it.hasNext()) {
		String str = it.next();
	}
	
	/*Iterator it = list.iterator();
	
	while(it.hasNext()) {
		//String str = (String)it.next();
		Object obj = it.next();
		
		
		System.out.println(str.length());
	}*/
}
}

(二)泛型类的定义

1、泛型类:带有泛型的类

2、格式:
class 类名<泛型1, 泛型2, 泛型3…> {}

3、说明:
(1)类后面跟着的泛型类型,是泛型的声明,一旦泛型声明出来,就相当于这个类未来会有一个已知类型,这个类型就可以在类中去使用了
(2)泛型类型的声明:只要是一个合法的标识符即可,一般使用单个的大写字母表示:T(Type)、E(Element)、K(Key)、V(Value)
(3)泛型确定的时机:将来使用这个类创建对象的时候,对象的泛型必须确定为具体的类型,对象的泛型确定为什么类型,类的泛型就跟着变成什么类型

代码示例

public class Demo02_Class {

public static void main(String[] args) {
	Test<String> t = new Test<>();
	t.show("abc");
	
	Test<Integer> t1 = new Test<>();
	t1.show(123);
	
}
}

class Test<T> {

public void show(T t) {
	System.out.println(t);
}

public T test() {
	return null;
}

}

(三)泛型类练习

定义一个带泛型的集合,只能在集合的头部增删(模拟Java虚拟机栈内存的运行机制,写一段代码)

代码示例

import java.util.LinkedList;

/**
 * 定义一个带泛型的集合,只能在集合的头部增删(模拟Java虚拟机栈内存的运行机制,写一段代码)
 * 
 * @author Zihuatanejo
 *
*/
public class Demo03_Exercise {

public static void main(String[] args) {
	//1.创建一个栈
	MyStack<String> ms = new MyStack<>();
	
	ms.push("aaa");
	ms.push("bbb");
	ms.push("ccc");
	ms.push("ddd");
	ms.push("eee");
	
	while(ms.capacity() > 0) {
		System.out.println(ms.pop());
	}
}
}

/**
 * 定义一个栈类型
 * 
 * @author Zihuatanejo
 *
 */
class MyStack<T> {

private LinkedList<T> list = new LinkedList<>();

//进栈
public void push(T t) {
	list.addFirst(t);
}

//弹栈
public T pop() {
	return list.removeFirst();
}

public int capacity() {
	return list.size();
}
}

(四)泛型方法的定义

1、在方法的声明中,带上泛型,就是泛型方法

2、格式:
权限修饰符 <泛型1, 泛型2, 泛型3…> 返回值类型 方法名称(参数列表) { 方法体 }

3、说明:
(1)在方法上声明泛型,可以在整个方法中作为已知类型来使用
(2)如果【非静态】方法没有声明任何泛型,可以直接使用类的泛型,此时泛型就会和类泛型确定时机一样,随着对象泛型的确定而确定;如果【非静态】方法上自己定义了泛型,泛型就会随着参数的类型进行变化
(3)【静态】方法,不能使用类的泛型,如果想要使用泛型,必须自己在方法声明上定义泛型。因为类泛型随对象确定,静态优先于对象存在,静态方法使用泛型的时候,对象很可能还未创建,泛型极有可能还不确定。

代码示例

public class Demo04_Method {

public static void main(String[] args) {
	Person<String> p = new Person<>();
	p.show3("qwe");
	
	Person<Integer> p1 = new Person<>();
	p1.show3("qwe");
	System.out.println(p1.show4(123));
}
}
//1.方法使用类的泛型
//2.方法自己定义泛型
//1.非静态方法
//2.静态方法
class Person<T> {

//1.方法使用类的泛型

public void show1(T t) {
	System.out.println(t);
}

public T show2() {
	return null;
}

//----------------

//2.方法自己定义泛型
//2.1 非静态方法自己定义泛型
public <E> void show3(E e) {//Object
	System.out.println(e);
}

public <E> E show4(E e) {//Object
	return e;
}

public <E> void show5(E e, T t) {
	
}	
//2.2 静态泛型方法
//类泛型随着对象的泛型确定而确定,静态方法随着类的加载而加载优先于对象存在
//如果静态方法使用类的泛型,对象就极有可能不存在,泛型也就确定不下来
//所以静态方法要想使用泛型,只能自己定义
public static <E> void show6(E e) {
	System.out.println(e);
}
}

(五)泛型方法的练习

定义一个方法,可以交换任意类型数组的两元素位置

import java.util.Arrays;

/**
 * 定义一个方法,可以交换任意类型数组的两元素位置
 * 
 * @author Zihuatanejo
 *
 */
public class Demo05_Exercise {

public static void main(String[] args) {
	//String[] arr = {"aaa", "bbb", "ccc", "ddd"};
	Integer[] arr = {111, 222, 333, 444};
	
	System.out.println(Arrays.toString(arr));
	
	swap(arr, 0, 3);
	
	System.out.println(Arrays.toString(arr));
}

/**
 * 交换数组两元素的位置
 * 
 * @param arr 待交换元素位置的数组
 * @param index1 要交换位置的元素索引
 * @param index2 要交换位置的元素索引
 */
public static <T> void swap(T[] arr, int index1, int index2) {
	T temp = arr[index1];
	arr[index1] = arr[index2];
	arr[index2] = temp;
}
}

(六)泛型接口的定义和使用

1、泛型接口:带泛型的接口

2、定义格式:
interface 接口名<泛型1, 泛型2, 泛型3…> {}

3、说明:

(1)在接口声明上,定义好泛型,整个接口中都可以将接口的泛型拿来使用

(2)泛型接口被其他类实现:
1)类实现接口的时候,泛型确定为了具体类型:
class 类名 implements 接口名<具体类型> {
实现接口的方法,方法使用了泛型,也都成了具体类型
}
2)类实现接口的时候,泛型依然不确定:
class 类名<泛型1, 泛型2…> implements 接口名<泛型1, 泛型2…> {
实现接口的方法,方法使用的泛型依然不确定
}
注意事项:累后面和接口后面的泛型符号要保持一致,表示同一个泛型;类实 现接口的时候,和原接口的泛型符号可以不一样

4、查看类或接口的快捷键:Ctrl + shift + t

代码示例

public class Demo06_Interface {

public static void main(String[] args) {
	
}
}

interface MyInter1<T> {

public abstract void test1(T t);

public abstract T test2();

public abstract T test3(T t);
}

//1.类将泛型确定为了具体类型
class Inter1 implements MyInter1<String> {

@Override
public void test1(String t) {
}

@Override
public String test2() {
	return null;
}

@Override
public String test3(String t) {
	return null;
}

}

//2.类没有将泛型确定为具体类型
class Inter2<E> implements MyInter1<E> {

@Override
public void test1(E t) {
}

@Override
public E test2() {
	return null;
}

@Override
public E test3(E t) {
	return null;
}
}

(七)泛型的通配符(了解)

1、符号:?

2、使用泛型的时候,没有使用具体的泛型声明【T】,而是使用了和T有关的类型,如果要表示和T有关的类型,就需要使用到泛型通配符【?】

3、第一种形式:使用?来表示可以使任意类型
在Collection中,removeAll(Collection<?> c)方法的参数,就表示可以接受任意类型泛型的集合,参数集合的泛型可以和调用者集合的泛型没有任何关系

import java.util.ArrayList;
import java.util.Collection;

public class Demo07 {

public static void main(String[] args) {
	//removeAll(Collection<?> c) 
	
	Collection<String> list = new ArrayList<>();
	Collection<Integer> list1 = new ArrayList<>();
	
	//假设调用者集合的泛型是A类型,参数集合的泛型可以是任意类型,此时?的作用就是通配
	list.removeAll(list1);
}
}

3、第二种格式:? extends E

在Collection集合中,方法addAll(Collection<? extends E> c)泛型所表示的就是:确定泛型的上边界。即:参数泛型是调用者泛型的本类或者子类,不能是父类,更不能是无关类

import java.util.ArrayList;
import java.util.Collection;

public class Demo07 {

public static void main(String[] args) {
	//addAll(Collection<? extends E> c) 
	
	Collection<String> list = new ArrayList<>();
	Collection<Integer> list1 = new ArrayList<>();
	Collection<Object> list2 = new ArrayList<>();
	Collection<String> list3 = new ArrayList<>();
	
	//list.addAll(list1);//String和Integer是无关类
	
	list.addAll(list2);//参数的泛型是调用者的泛型的父类
	
	list2.addAll(list1);//参数泛型是调用者泛型的子类
	list2.addAll(list);
	list.addAll(list3);//参数泛型和调用者泛型是同类
}

public static void test1() {
	//removeAll(Collection<?> c) 
	
	Collection<String> list = new ArrayList<>();
	Collection<Integer> list1 = new ArrayList<>();
	
	//假设调用者集合的泛型是A类型,参数集合的泛型可以是任意类型,此时?的作用就是通配
	list.removeAll(list1);
}
}

4、第三种情况:? super E

确定泛型的下边界:表示泛型是E的父类或者是E本身,不能是E的子类,更不能是无关类。

二、Set

(一)Set概述

1、Set是Collection的子接口

2、特点:
(1)无序:存取顺序不一致
(2)不可重复:不能存储重复的元素
(3)不可重复的原因:不像List集合有索引可以区分不同元素

3、实现类:
(1)HashSet,底层是哈希表
(2)LinkedHashSet,底层是链表加哈希表

4、存储特点:
(1)相同元素无法存储(元素不可重复)
(2)存取顺序不一致

代码示例

import java.util.HashSet;
import java.util.Set;

public class Demo08_Set {

public static void main(String[] args) {
	//创建对象的时候,创建的是实现类的对象
	Set<String> set = new HashSet<>();
	
	//无序,不可重复
	set.add("qwe");
	set.add("qwe");
	set.add("asdfghj");
	set.add("asdfghj");
	set.add("asdfghj");
	set.add("12uiou");
	set.add("12uiou");
	set.add("#$%^&");
	set.add("#$%^&");
	set.add("#$%^&");
	set.add("-09876789");
	set.add("-09876789");
	set.add(null);
	set.add(null);
	
	System.out.println(set);
}
}

(二)Set集合的遍历

1、没有自己特有的方法,使用Collection中的方法,使用Collection遍历的方式来遍历Set集合

2、第一种:toArray()转数组,遍历数组

3、第二种:T[] toArray(T[] a) 转数组,遍历数组
(1)当数组的长度等于元素个数时,就是用提供的数组
(2)当数组的长度小于元素个数时,底层创建新的数组存储集合元素
(3)当数组长度大于元素个数时,依然使用提供的数组,但是数组剩余的部分就按照默认值填充

4、第三种:迭代器

5、第四种:增强for循环
(1)格式:
for(元素的数据类型 元素名称: 要遍历的集合或者数组) {
使用元素名称操作元素
}
(2)说明:增强for循环的底层是迭代器,如果增强for循环遍历几个的过程中,使用了集合对象修改集合,会出现并发修改异常

代码示例

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class Demo09_PrintSet {

public static void main(String[] args) {
	Set<Integer> set = new HashSet<>();
	
	set.add(0);
	set.add(0);
	set.add(324);
	set.add(324);
	set.add(11);
	set.add(11);
	set.add(-97);
	set.add(-97);
	set.add(86);
	set.add(86);
	set.add(83);
	set.add(83);
	set.add(19);
	set.add(19);
	
}

public static void forthWay() {
	int[] arr = {1, 2, 3, 4, 5};
	
	/*for (Integer num : set) {
		
	}*/
	
	/*
	 * 增强for循环
	 *  for(元素的数据类型  元素名称: 要遍历的集合或者数组) {
			使用元素名称操作元素
		}
	 * 
	 * 迭代器遍历,集合修改
	 * 
	 * */
	/*for(Integer ele: set) {
		if (ele.equals(-97)) {
			set.remove(-97);//集合修改
		}
	}*/
	
	/*int[] arr = {1, 2, 3, 4, 5};
	
	for(int ele: arr) {
		System.out.println(ele);
	}*/
}

public static void thirdWay(Set<Integer> set) {
	//1.获取集合中的迭代器对象
	Iterator<Integer> it = set.iterator();
	
	//2.迭代器遍历集合
	while(it.hasNext()) {
		System.out.println(it.next());
	}
}

public static void secondWay(Set<Integer> set) {
	/*
	 * 1.当数组的长度等于元素个数时,就是用提供的数组
	 * 2.当数组的长度小于元素个数时,底层创建新的数组存储集合元素
	 * 3.当数组长度大于元素个数时,依然使用提供的数组,但是数组剩余的部分就按照默认值填充
	 * 
	 * */
	
	//1.准备数组
	//Integer[] arr = new Integer[set.size()];
	//Integer[] arr = new Integer[3];
	Integer[] arr = new Integer[10];
	
	//2.调用toArray(T[] arr)
	Integer[] arr1 = set.toArray(arr);
	
	//3.遍历数组
	for (int i = 0; i < arr1.length; i++) {
		System.out.print(arr1[i] + " ");
	}
	
	System.out.println(arr);
	System.out.println(arr1);
}

public static void firstWay() {
	Set<Integer> set = new HashSet<>();
	
	set.add(0);
	set.add(0);
	set.add(324);
	set.add(324);
	set.add(11);
	set.add(11);
	set.add(-97);
	set.add(-97);
	set.add(86);
	set.add(86);
	set.add(83);
	set.add(83);
	set.add(19);
	set.add(19);
	
	//1.调用toArray()方法,将集合转为数组
	Object[] arr = set.toArray();
	
	//2.遍历数组
	for (int i = 0; i < arr.length; i++) {
		System.out.print(arr[i] + " ");
	}
}
}

(三)Set练习

随机生成10个20~40的随机数,随机数不能重复,存储到一个集合中,遍历集合

代码示例

import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/**
 * 随机生成10个20~40的随机数,随机数不能重复,存储到一	个集合中,遍历集合
 * 
 * @author Zihuatanejo
 *
 */
public class Demo10_Exericse {

public static void main(String[] args) {
	//1.Random
	//2.Math
	
	//创建Set集合,用于存储数据并且能够自动去重
	Set<Integer> set = new HashSet<>();
	
	//1.创建Random对象
	Random r = new Random();
	
	while (set.size() < 10) {
		
		//2.调用Random类型中的nextInt(int n)方法,生成[0, n)
		int ranNum = r.nextInt(21) + 20;
		
		set.add(ranNum);
	}
	
	for (Integer ele : set) {
		System.out.println(ele);
	}
}
}

三、HashSet保证元素唯一性的原理

(一)HashSet存储JDK提供类型的元素

HashSet能够对JDK提供类型的元素进行有效去重

代码示例

import java.util.HashSet;

public class Demo11_HashSetSaveJdkElement {

public static void main(String[] args) {
	HashSet<Object> set = new HashSet<>();
	
	set.add(123);
	set.add(123);
	set.add(1.2);
	set.add(1.2);
	set.add('c');
	set.add('c');
	set.add(false);
	set.add(false);
	set.add("qwe");
	set.add("qwe");
	
	System.out.println(set);
}
}

(二)HashSet对自定义类型元素去重

实验过程:
1、在HashSet中存储自定义类型Person的对象

2、打印集合,发现自定义类型的元素没有去重

3、怀疑是没有重写equals方法,每一个对象的判断是按照重写前equals判断的,即按照地址判断

4、重写equals方法,发现依然没有去重

5、怀疑equals方法没有被执行,在重写的equals方法中书写显式输出打印的语句,发现确实没有被执行

6、怀疑还是和地址值相关

7、分析地址值,组成部分:全类名@十六进制数字;所有对象的全类名一样,因为来自于同一个类;@没有讨论的必要,就是一个普通的分隔符;所以问题一定处在十六进制数字

8、十六进制数字的由来:根据对象的真实地址,通过hashCode方法计算出哈希码值,再将哈希码值转为十六进制数字,所以:问题一定处在hashCode身上

9、我们不妨结合hashCode方法的常规协定三:让不同的对象具有相同的哈希码值,如果具有了相同的哈希码值,就意味着所有对象有同一个地址,这样就无法通过地址去区分不同的对象了

10、发现重写hashCode让所有的对象具有相同哈希码值,进而拥有相同地址值,无法通过不同的地址区分不同的对象,再次运行,equlas方法执行了,去重也实现了

代码示例

import java.util.HashSet;

public class Demo12_HashSetSaveDefinedClassElement {

public static void main(String[] args) {
	//地址的组成部分:全类名@十六进制数字

	HashSet<Person> set = new HashSet<>();
	
	set.add(new Person("zhangsan", 23));
	set.add(new Person("zhangsan", 23));
	set.add(new Person("zhangsan", 23));
	set.add(new Person("lisi", 24));
	set.add(new Person("lisi", 24));
	set.add(new Person("lisi", 24));
	set.add(new Person("wangwu", 25));
	set.add(new Person("wangwu", 25));
	set.add(new Person("wangwu", 25));
	set.add(new Person("zhaoliu", 26));
	set.add(new Person("zhaoliu", 26));
	set.add(new Person("zhaoliu", 26));
	
	System.out.println(set);
}
}


public class Person {
private String name;
private int age;

public Person() {
	super();
}

public Person(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 "Person [name=" + name + ", age=" + age + "]";
}

@Override
public int hashCode() {
	return 0;
}

@Override
public boolean equals(Object obj) {
	System.out.println("重写后的equals方法执行了。。。。");
	Person p = (Person)obj;
	
	return this.name.equals(p.name) && this.age == p.age;
}
}

(三)HashSet保证元素唯一性原理总结

1、某个对象obj,即将要存储到HashSet集合的时候,首先要计算对象的哈希码值

2、在集合中所有元素的哈希码值,都和obj的哈希码值不同,就直接将obj对象进行存储

3、如果集合中存在元素和obj对象的哈希值相同,并不能说明obj就在集合中存在

4、会进而调用equals方法判断obj对象和集合中对象是否相等

5、如果不相等,就对obj对象进行存储,如果相等则去重

图示1:
在这里插入图片描述

图示2:

在这里插入图片描述

(四)保证元素唯一性的操作

1、重写hashCode方法,让不同的对象具有相同的哈希码值,不同的对象尽量要有不同的哈希码值

2、重写equals方法,通过属性值来区分不同的对象

3、最终操作:alt + shift + s H

代码示例

import java.util.HashSet;

public class Demo12_HashSetSaveDefinedClassElement {

public static void main(String[] args) {
	//地址的组成部分:全类名@十六进制数字
	
	HashSet<Person> set = new HashSet<>();
	
	/*set.add(new Person("zhangsan", 23));
	set.add(new Person("zhangsan", 23));
	set.add(new Person("zhangsan", 23));
	set.add(new Person("lisi", 24));
	set.add(new Person("lisi", 24));
	set.add(new Person("lisi", 24));
	set.add(new Person("wangwu", 25));
	set.add(new Person("wangwu", 25));
	set.add(new Person("wangwu", 25));
	set.add(new Person("zhaoliu", 26));
	set.add(new Person("zhaoliu", 26));
	set.add(new Person("zhaoliu", 26));*/
	
	System.out.println(new Person("张三", 23).hashCode());
	System.out.println(new Person("李四", 24).hashCode());
	System.out.println(new Person("王五", 25).hashCode());
	
	System.out.println(2018699554 % 16);
	System.out.println(1311053135 % 16);
	System.out.println(118352462 % 16);
	
	System.out.println(set);
}
}
package com.offcn.demos;

public class Person {
private String name;
private int age;

public Person() {
	super();
}

public Person(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 "Person [name=" + name + ", age=" + age + "]";
}

@Override//相同对象一定要有相同的哈希值,不同对象尽量有不同哈希值
public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result + age;
	result = prime * result + ((name == null) ? 0 : name.hashCode());
	return result;
}

@Override
public boolean equals(Object obj) {
	if (this == obj)
		return true;
	if (obj == null)
		return false;
	if (getClass() != obj.getClass())
		return false;
	Person other = (Person) obj;
	if (age != other.age)
		return false;
	if (name == null) {
		if (other.name != null)
			return false;
	} else if (!name.equals(other.name))
		return false;
	return true;
}

/*@Override
public int hashCode() {
	return 0;
}*/

/*@Override
public boolean equals(Object obj) {
	System.out.println("重写后的equals方法执行了。。。。");
	Person p = (Person)obj;
	
	return this.name.equals(p.name) && this.age == p.age;
}*/
}

四、LinkedHashSet

(一)LinkedHashSet

1、是HashSet的一个子类,和HashSet保证元素唯一性的原理相同

2、和HashSet的不同之处在于:存取顺序一致

3、特点:有序,不可重复

4、应用:既要保证顺序,又要去重的时候,可以考虑使用

代码示例

import java.util.LinkedHashSet;

public class Demo13_LinkedHashSet {

public static void main(String[] args) {
	LinkedHashSet<String> set = new LinkedHashSet<>();
	
	set.add("qwe");
	set.add("qwe");
	set.add("asdfghj");
	set.add("asdfghj");
	set.add("asdfghj");
	set.add("12uiou");
	set.add("12uiou");
	set.add("#$%^&");
	set.add("#$%^&");
	set.add("#$%^&");
	set.add("-09876789");
	set.add("-09876789");
	set.add(null);
	set.add(null);
	
	System.out.println(set);
}
}
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值