Java基础知识学习:简单随手记录(3)

学习视频链接:https://www.bilibili.com/video/BV1fh411y7R8?p=1&vd_source=1635a55d1012e0ef6688b3652cefcdfe
(本文出现的程序和图片参考视频)

一.枚举和注解

  • 枚举是一组常量的集合。
  • 枚举属于一种特殊的类,里面只包含一组有限的特定的对象。

自定义类实现枚举

  1. 构造器私有化
  2. 本类内部创建一组对象[四个 春夏秋冬]
  3. 对外暴露对象(通过为对象添加 public final static 修饰符)
  4. 可以提供 get 方法,但是不要提供 set
public class myCode {
    public static void main(String[] args) {
        System.out.println(Season.SPRING);
    }
}
class Season {
    private String name;
    private String desc;

    //2.在内部直接创建固定的对象
    //public是为了外面可以使用,static是为了外面不创建对象,用类名就能使用
    public static final Season SPRING = new Season("春天", "温暖");
    public static final Season WINTER = new Season("冬天", "寒冷");
    
    //说实话这里没看出来加final有什么意义,右边都有new,那就肯定会加载类,难道只是因为static和final经常一起使用?
//    public static Season SPRING = new Season("春天", "温暖");
//    public static Season WINTER = new Season("冬天", "寒冷");

    //1.将构造器私有化,防止在外面创建对象(注意要去掉set相关方法,防止属性被修改)
    private Season(String name, String desc) {
        System.out.println("调用构造函数!!");
        this.name = name;
        this.desc = desc;
    }

    public String getName() {
        return name;
    }

    public String getDesc() {
        return desc;
    }

    @Override
    public String toString() {
        return "Season{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
}

enum关键字实现枚举

1.使用关键字 enum 替代 class
2.public static final Season SPRING = new Season(“春天”, “温暖”) 直接使用 SPRING(“春天”, “温暖”) 解读 常量名(实参列表)
3.如果有多个常量(对象), 使用 ,号间隔即可
4.如果使用 enum 来实现枚举,要求将定义常量对象,写在前面
5.如果我们使用的是无参构造器,创建常量对象,则可以省略 ()

public class myCode {
    public static void main(String[] args) {
        Season season = Season.SPRING;
        Season season2 = Season.SPRING;

        System.out.println(Season.SPRING);
        System.out.println(Season.SUMMER);
        System.out.println(Season.TSEASON);
        System.out.println(season==season2);//同一个对象
    }
}
enum Season {
    SPRING("春天", "温暖"), SUMMER("夏天", "炎热"), TSEASON;
    private String name;
    private String desc;

    private Season(){}
    private Season(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }

    public String getName() {
        return name;
    }

    public String getDesc() {
        return desc;
    }

    @Override
    public String toString() {
        return "Season{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
}

enum 实现接口

1)使用 enum 关键字后,就不能再继承其它类了,因为 enum 会隐式继承 Enum,而 Java 是单继承机制。
2)枚举类和普通类一样,可以实现接口,如下形式。
enum 类名 implements 接口 1,接口 2{}
------------------------------------------------------------------------------------------------------------------------------------------

  1. 注解(Annotation)也被称为元数据(Metadata),用于修饰解释 包、类、方法、属性、构造器、局部变量等数据信息。
  2. 和注释一样,注解不影响程序逻辑,但注解可以被编译或运行,相当于嵌入在代码中的补充信息。

三个基本的 Annotation(待学习)

  1. @Override: 限定某个方法,是重写父类方法, 该注解只能用于方法;
    如果写了@Override 注解,编译器就会去检查该方法是否真的重写了父类的方法,如果的确重写了,则编译通过,如果没有构成重写,则编译错误

  2. @Deprecated: 用于表示某个程序元素(类, 方法等)已过时;

  3. @SuppressWarnings: 抑制编译器警告;

二.异常

  1. NullPointerException 空指针异常
  2. ArithmeticException 数学运算异常
  3. ArrayIndexOutOfBoundsException 数组下标越界异常
  4. ClassCastException 类型转换异常
  5. NumberFormatException 数字格式不正确异常
//可以用异常来实现用户输入整数
import java.util.Scanner;
public class myCode {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            try {
                System.out.print("请输入整数:");
                //当输入带小数点或者其他非整数的字符都会有异常
                Integer.parseInt(scanner.next());
                break;
            } catch (NumberFormatException e) {
                System.out.println("请不要输入非数字的信息!");
            }
        }
    }
}

异常处理机制

在这里插入图片描述
简单判断题:(最后输出的是什么?)

public class myCode {
    public static void main(String[] args) {
        System.out.println(method());
    }

    public static int method() {
        int i = 1;
        try {
            i++;//i+1=2
            String[] names = new String[3];//全为null
            if (names[2].equals("gg")) {
                //显然会抛出空指针异常,连语句都执行不了
                System.out.println(names[2]);
            } else {
                //上面就异常了,就不会执行到这里了
                names[3] = "gg";
            }
            return 1;
        } catch (ArrayIndexOutOfBoundsException e) {
            //显然空指针异常就不执行后面的程序了,是不会到names[3] = "gg"这句的越界异常的
            return 2;
        } catch (NullPointerException e) {
            return ++i;//i+1=3
            //注意下面还有一个finally,要等下面执行完了再执行(这里的return一定是返回3,下面的i再变都不影响这里的返回)
            //最后返回3,一定要清楚执行的顺序以及输出的内容
        } finally {
            ++i;//i+1=4
            System.out.println("i = " + i);//输出"i = 4"
        }
    }
}

throws 异常处理

如果一个方法可以生成某种异常,但是又不能确定如何处理这些异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,由该方法的调用者负责处理
1.对于编译时异常,程序中必须处理(没有默认方法,必须显式处理),比如 try-catch 或者 throws
2.对于运行时异常,程序中如果没有处理,默认就是 throws,就比如num2=0,而num1/num2程序在编写时没有提示错误就是因为默认有一个throws处理,将这个运行时错误抛给上一层,如果都没有明确处理的方式,就是最终给到JVM进行处理,而JVM的处理方式就是运行到那个除以0的时候就直接中止程序,给出错误提示;
3. 子类重写父类的方法时,对抛出异常的规定:子类重写的方法,所抛出的异常类型要么和父类抛出的异常一致,要么为父类抛出的异常类型的子类型
4. 在 throws 过程中,如果有方法 try-catch , 就相当于处理异常,就可以不必 throw

自定义异常

public class CustomException {
	public static void main(String[] args) /*throws AgeException*/ {
		int age = 180;
		//要求范围在 18 – 120 之间,否则抛出一个自定义异常
		if(!(age >= 18 && age <= 120)) {
			//这里可以通过构造器设置信息
			throw new AgeException("年龄需要在 18~120 之间");
		}
		System.out.println("你的年龄范围正确.");
	}
}
class AgeException extends RuntimeException {
	public AgeException(String message) {//构造器
		super(message);
	}
}
  • 补充1: 如果下面的程序没有出现编译时的错误,就没必要写throws Exception,写了throws就要处理(不管原代码有没有错)
    补充2:final块执行完之后才会回来执行try或catch中的return或throw语句

try/catch-final执行顺序讨论:如果finally存在,任何执行try 或者catch中的return语句之前,都会先执行finally语句。如果finally中有return语句,那么程序就return了,所以finally中的return是一定会被return的。

补充:finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的
情况一:顺序执行

try{

} catch() {

} finally {

}
return 1;

情况二:程序执行try块中return之前(包括return语句中的表达式运算)代码;再执行finally块,最后执行try中return 1;finally块之后的语句return 2,因为程序在try中已经执行return 1,所以不再执行。

try{
	return 1} catch() {

} finally {

}
return 2;

情况三:程序执行try块中代码,如果遇到异常,执行catch块中return之前(包括return语句中的表达式运算)代码,再执行finally语句中全部代码, 最后执行catch块中return 1,finally之后的return 2不再执行。如果没有遇到异常,执行完try再finally再return 2

try{
	
} catch() {
	return 1} finally {

}
return 2;

情况四:程序执行try块中return之前(包括return语句中的表达式运算)代码,再执行finally块,因为finally块中有return 2所以就直接退出。

try{
	return 1} catch() {
	
} finally {
	return 2;
}

情况五:程序执行try块中代码,再执行finally块,因为finally块中有return 2所以就直接退出。

try{
	
} catch() {
	return 1} finally {
	return 2;
}

情况六:程序执行try块中return之前(包括return语句中的表达式运算)代码,有异常就执行catch块中return之前(包括return语句中的表达式运算)代码,再执行finally块,因为finally块中有return所以提前退出。
无异常就再执行finally块,因为finally块中有return所以提前退出。

try{
	return 1} catch() {
	return 2} finally {
	return 3;
}
//简单题目
public class myCode {
    public static void main(String[] args) throws Exception{
        try {
            ReturnExceptionDemo.methodA();//第一步:执行methodA
        } catch (Exception e) {//第五步,接住前面抛出的信息
            System.out.println(e.getMessage());//第六步
        }
        ReturnExceptionDemo.methodB();//第七步
    }
}
class ReturnExceptionDemo{
    static void methodA() {
        try {
            System.out.println("进入方法A");//第二步,执行完看下一句是否为throw或者return,如果是就执行final
            throw new RuntimeException("制造异常");//第四步,根据定义的异常信息建对象实例
        } finally {
            System.out.println("A方法的final");//第三步
        }
    }
    static void methodB() {
        try {
            System.out.println("进入方法B");//第八步
            return;//第十步
        } finally {
            System.out.println("B方法的final");//第九步
        }
    }
}

三.包装类

在这里插入图片描述
Boolean和Character都是继承Object,而后面那几个是继承Number(Number继承Object)

Integer

//包装类与基本数据类型的装箱和拆箱
int n1 = 100;
//手动装箱的两种语句
Integer integer = new Integer(n1);
Integer integer1 = Integer.valueOf(n1);
//自动装箱
Integer integer3 = n1; //底层使用的是 Integer.valueOf(n1),即上面的语句

//手动拆箱
int i = integer.intVal;
//自动拆箱
int n2 = integer1; //底层仍然使用的是 intValue()方法

(小提醒:三目运算符能提升精度, Object obj = false ? new Double(2.5):new Integer(1) ;输出1.0)

//包装类型与String类型的转换
Integer i = 100;
String str = i.toString();
Integer i_ = Integer.parseInt(str);

注意Integer的创建形式(装箱)
在这里插入图片描述

public class myCode {
    public static void main(String[] args) {
        Integer i = new Integer(1);//明确使用new创建对象
        Integer j = new Integer(1);//明确使用new创建对象
        System.out.println(i == j);//对象之间比地址,显然上面是不同的对象false

        //通过valueOf源码可知,Integer这个类加载时就会生成一个数组,数组包括-128~127,只要使用自动装箱的形式就会直接将
        //需要构建的对象实例直接指向这个数组对应的数字位置
        Integer m = 1;//使用自动装箱,注意底层的机制
        Integer n = 1;
        System.out.println(m == n);//指向同一个对象地址,true

        Integer x = 128;//使用自动装箱,注意底层的机制
        Integer y = 128;
        System.out.println(x == y);//由于超出127,这里会给x,y都生成一个新的对象,false
    }
}

注意Integer和int之间的比较

Integer i=129;
int j=129;
//有出现基本数据类型,判断的就是值是否相同
System.out.println(i==j); //true

String(有很多构造器)

  • String类继承Object,并实现Serializable、Comparable、CharSequence三个接口
    实现Serializable接口表明String可以在进行网络传输(串行化)
    实现Comparable接口表明不同的String对象实例之间可以进行比较
  • String有属性private final char value[],用于存放字符串内容,final修饰的是引用数据类型,就说明地址不能修改,不要误解为值不能修改

注意String的创建形式
方式一:直接赋值 String str = “abc”;
方式二:调用构造器 String str2 = new String(“abc”);
对于方式一来说,先检查常量池中是否有"abc"数据空间,如果有就将str直接指向,如果吗,没有就重新创建,然后指向,str最终指向的是常量池的空间地址
对于方式二来说,先在堆中创建空间,里面维护value属性(就是常量池的地址),指向常量池的abc空间,如果常量池里没有abc就重新创建,如果有就直接通过value指向,最终str2指向的是堆中的空间地址
(小提醒:intern方法返回的是字符串在常量池中的地址,就比如如果是调用构造器来生成字符串对象b,直接看到的b就是对中的地址,而b.intern()就是堆中存放的那个数据的常量池地址【简单的说调用构造器就是保存中转站地址,而intern就是得到实际最终的地址】)

public class myCode {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.name = "abc";
        Person p2 = new Person();
        p2.name = "abc";
        System.out.println(p1.name == p2.name);//比较的是p1.name和p2.name指向的地址,true
        System.out.println(p1.name == "abc");//比较的是p1.name和"abc"指向的地址,true
        // 分析:p1和p2两个对象的name都是用直接赋值的形式实现的,因此p1.name和p2.name里存放的就是常量池里“abc”的地址
    }
}
class Person {
    public String name;
}

在这里插入图片描述

//问下列语句创建多少个对象?(三道题)
        //题一(两个)
        String s1 = "hello";
        s1 = "haha";
        //分析:在常量池中先建立hello对象,然后将s1直接指向常量池的hello,后面再新建haha对象,在令s1指向haha
        //所以是创建了两个在常量池的对象

        //题二(一个)
        String temp = "abc" + "ADC";
        //对于这种形式的写法,底层其实只是创建一个对象"abcADC",不会创建没有变量指向的对象
        //编译器会做一个优化,判断创建的常量池对象是否有引用指向

        //题三(三个)
        String a = "hello";
        String b = "abc";
        String c = a + b;
        //String c = a + b;的底层执行如下
        //StringBuilder sb = new StringBuilder();sb.append(a);sb.append(b);
        // sb是在堆中的(存放常量池某个对象的地址),append是在原来字符的基础上添加的
        //String temp = "abc" + "ADC";是在常量池中相加,String c = a + b;是在堆中相加,得到的是一个指向常量池对象
        //地址,所以会创建一个新的常量池对象?(因此只要涉及String类型的对象名进行加减都是新建对象(如a + "abc")?)

回顾一下值传递:
1.基本数据类型的局部变量以及数据都是直接存储在内存中的栈上。基本数据类型的数据本身是不会改变的,当局部变量重新赋值时,并不是在内存中改变字面量内容,而是重新在栈中寻找已存在的相同的数据,若栈中不存在,则重新开辟内存存新数据,并且把要重新赋值的局部变量的引用指向新数据所在地址。
2.基本数据类型的成员变量(在类体中定义的变量)名和值都存储于堆中,其生命周期和对象的是一致的。
3.基本数据类型的静态变量名以及值存储于方法区的运行时常量池中,静态变量随类加载而加载,随类消失而消失。

4.引用类型作为参数传递时,都会创建一个对象引用(实参)的副本(形参),该形参保存的地址和实参一样。下面看一个例子(例子原文链接:https://www.zhihu.com/question/385114001/answer/1393887646)

public static void main(String[] args) {
        Person a = new Person(18);
        Person b = new Person(18);
        modify(a, b);

        System.out.println(a.getAge());
        System.out.println(b.getAge());
    }

    private static void modify(Person a1, Person b1) {
        a1.setAge(30);

        b1 = new Person(18);
        b1.setAge(30);
    }

在调用modify之前(a、b内存如下所示,左边是栈,右边是堆)
在这里插入图片描述
调用modify时,a、b都创建一个新的副本a1、b1(也都是指向a、b的地址的对象引用)
在这里插入图片描述
当modify修改a1的age为30,意味也a的age也修改为30,而b1是new一个新的对象,也就是b1成为了另一个新的对象的对象引用(只有不涉及修改地址指向的操作才会对原来的b有影响,也就是说对一个对象的具体属性值或者char数组具体某个下标的值进行操作,直接用对象名或数组名操作相当于是改变指向的地址?),对b1的修改只是修改新对象的age
在这里插入图片描述

StringBuffer

  • StringBuffer是一个容器,代表可变的字符序列,可以对字符串进行增删
  • StringBuffer是final类,继承抽象类AbstractStringBuilder(AbstractStringBuilder有属性char[] value,注意这个value不是final的,所以地址是可变的,用于存放字符序列),实现了Serializable接口
  • String和StringBuffer的对比:String保存的是字符串常量(内部的value是final),每次String类的更新实际上就是更改地址(个人理解:因为原来的string直接指向的常量池对象长度是固定的,如果我要修改更长的内容,原对象就装不下了,只能生成新的对象后再让对象引用指向新的地址)
  • StringBuffer保存的是字符串变量,里面的值可变,StringBuffer的更新实际上可以更新内容,不用每次更新地址
    (个人理解:只有在更新长度时才会更改堆中的value地址)
  • String与StringBuffer
String str = "hello tom";
//String——>StringBuffer
//方式 1 使用构造器
//注意:返回的才是 StringBuffer 对象,对 str 本身没有影响
StringBuffer stringBuffer = new StringBuffer(str);
//方式 2 使用的是 append 方法
StringBuffer stringBuffer1 = new StringBuffer();
stringBuffer1 = stringBuffer1.append(str);

//StringBuffer ->String
StringBuffer stringBuffer2 = new StringBuffer("韩顺平教育");
//方式 1 使用 StringBuffer 提供的 toString 方法
String s = stringBuffer2.toString();
//方式 2: 使用构造器来搞定
String s1 = new String(stringBuff2);
//测试题
String str = null;// ok
StringBuffer sb = new StringBuffer(); //ok
// 使用的是 append 方法
sb.append(str);//需要看源码 , 底层调用的是 AbstractStringBuilder 的 appendNull
System.out.println(sb.length());//4
System.out.println(sb);//null

//使用构造器
StringBuffer sb1 = new StringBuffer(str);//看底层源码 super(str.length() + 16);而str为空,抛出空指针异常
System.out.println(sb1);

StringBuild

  • 1.StringBuilder 继承 AbstractStringBuilder 类

  • 2.实现了 Serializable ,说明 StringBuilder 对象是可以串行化(对象可以网络传输,可以保存到文件)

  • 3.StringBuilder 是 final 类, 不能被继承

  • 4.StringBuilder 对象字符序列仍然是存放在其父类 AbstractStringBuilder 的 char[] value;因此,字符序列是在堆中的

  • 5.StringBuilder 的方法,没有做互斥的处理,即没有 synchronized 关键字,因此在单线程的情况下使用

  • 如果字符串存在大量的修改操作,一般使用 StringBuffer 或 StringBuilder;

  • 如果字符串存在大量的修改操作,并在单线程的情况,使用 StringBuilder;

  • 如果字符串存在大量的修改操作,并在多线程的情况,使用 StringBuffer;

  • 如果字符串很少修改,被多个对象引用,使用 String ,比如配置信息等;

四.集合

在这里插入图片描述

Collection的接口和常用方法

  • 1.collection实现子类可以存放多个元素,每个元素可以是Object
  • 2.有些ColIection的实现类,可以存放重复的元素,有些不可以
  • 3.有些Collection的实现类,有些是有序的( List ) ,有些不是有序( Set )
  • 4.Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的
public static void main(String[] args) {
	List list = new ArrayList();
	// add:添加单个元素
	list.add("jack");
	list.add(10);//list.add(new Integer(10))
	list.add(true);
	System.out.println("list=" + list);
	// remove:删除指定元素
	//list.remove(0);//删除第一个元素
	list.remove(true);//指定删除某个元素
	System.out.println("list=" + list);
	// contains:查找元素是否存在
	System.out.println(list.contains("jack"));//T
	// size:获取元素个数
	System.out.println(list.size());//2
	// isEmpty:判断是否为空
	System.out.println(list.isEmpty());//F
	// clear:清空
	list.clear();
	System.out.println("list=" + list);
	// addAll:添加多个元素
	ArrayList list2 = new ArrayList();
	list2.add("红楼梦");
	list2.add("三国演义");
	list.addAll(list2);
	System.out.println("list=" + list);
	// containsAll:查找多个元素是否都存在
	System.out.println(list.containsAll(list2));//T
	// removeAll:删除多个元素
	list.add("聊斋");
	list.removeAll(list2);
	System.out.println("list=" + list);//[聊斋]
}

Collection 接口遍历元素方式:使用 Iterator(迭代器) 、for循环增强、普通for循环
注:调用iterator.next()之前一定要调用iterator.hasNext()进行检测,若不调用且下条记录无效时就会抛出NoSuchElementException异常

public static void main(String[] args) {
	Collection col = new ArrayList();
	col.add(new Book("三国演义", "罗贯中", 88.0));
	col.add(new Book("水浒传", "施耐庵", 30.5));
	col.add(new Book("红楼梦", "曹雪芹", 66.6));
	//1. 先得到 col 对应的 迭代器
	Iterator iterator = col.iterator();
	while (iterator.hasNext()) {
		Object obj = iterator.next();
		System.out.println("obj=" + obj);
	}
	//2. 当退出 while 循环后 , 这时 iterator 迭代器,指向最后的元素
	// iterator.next();//抛出NoSuchElementException异常
	//3. 如果希望再次遍历,需要重置我们的迭代器:即iterator = col.iterator();
}
class Book {
	private String name;
	private String author;
	private double price;
	public Book(String name, String author, double price) {
		this.name = name;
		this.author = author;
		this.price = price;
	}
}
Collection下的子接口List
  • List集合类中元素有序(即添加和取出顺序一致)、且可以重复
  • List集合中的每个元素都有对应的顺序索引,即支持索引
  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
  • 常用的list接口实现的子类有ArrayList、LinkedList和Vector

void add(int index, Object ele):在 index 位置插入ele元素
boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
Object get(int index):获取指定 index 位置的元素
int indexOf(Object obj):返回 obj 在集合中首次出现的位置
int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
Object remove(int index):移除指定 index 位置的元素,并返回此元素
Object set(int index, Object ele):设置指定 index 位置的元素为 ele , 相当于是替换
List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合

ArrayList底层和源码结构
  • ArrayList可以加入多个null
  • ArrayList基本是由数组来实现数据存储的
  • ArrayList基本等同于Vector,除了ArrayList是线程不安全

ArrayList底层机制的个人理解:
涉及三个变量:当前添加元素后所需的最小容量minCapacity、当前ArrayList的总长度(包括NULL值的长度)elementData.length(这当然这是oldCapacity ),新容量newCapacity
一开始如果只是new一个ArrayList对象时(无参),初始的elementData容量为0,当有元素第一次添加到ArrayList时elementData才会扩容到自定义的最小容量(10),接下来每一次添加元素都会判断需不需要扩容,判断的条件就是minCapacity - elementData.length > 0?
假设需要扩容,就会先计算elementData.length * 1.5后的长度记为newCapacity(一般都可以了),旧的elementData.length当然就记为oldCapacity,然后再用newCapacity - minCapacity < 0?(如果真的满足这个条件了就不是一般情况了),一般都不满足这个条件,接下来就拿着这个newCapacity来进行真正的ArrayList扩容。

Vector底层结构
  • Vector底层也是一个对象数组
  • Vector是线程同步的,即线程安全的,在开发中如果考虑线程安全就优先使用vector
  • 无参构造第一次默认10个,每次扩容2倍(有参构造可以自己设置次扩容的大小,所有在计算newCapaticy时和ArrayList优点区别,其他基本是一致的流程)
LinkedList底层结构(源码就是链表的操作)
  • LinkedList底层实现了双向链表和双端队列的特点
  • 可以添加任何元素(重复也可以)包括NULL
  • 线程不安全,没有实现同步
  • LinkedList中维护了两个属性first和last,分别指向首节点和尾结点
  • 每个节点(Node对象)有维护prev、next、item三个属性

ArrayList和LinkedList的比较:ArrayList底层是可变数组,增删效率较低(因为有数组扩容),改查的效率较高;LinkedList的底层是双向链表,增删的效率较高,改查的效率较低(两个都是线程不安全的)。

Collection下的子接口Set

遍历方式:迭代器、增强for、toArray;(注意:不能使用索引的方式获取)
set 接口的实现类的对象(Set 接口对象), 不能存放重复的元素, 可以添加一个 null
set 接口对象存放数据是无序(即添加的顺序和取出的顺序不一致

Set 接口实现类-HashSet
  • HashSet其实是HashMap(数组加链表加红黑树的结构)
  • 可以存放一个NULL值(因为不能重复)
  • HashSet不保证元素是有序的,取决于hash后再确认索引的结果
  • 如果用引用对象作为参数,其实就是看对象的地址,同名但是地址是不同的两个对象(引用对象的equals就是默认比较地址),注意String重写了equals

HashSet扩容机制
1.添加一个元素时先得到hash值(注意在程序实现时会将计算得到的hash值右移16位,是为了尽量减少和其他hash值冲突,所以最终得到的hash值不是真正的hash值),会转换为索引值(数组位置)
2.找到存储数据表table,看这个索引位置是否已经存放有相同的元素,没有就直接加入,有就调用equals进行比较(具体比较上面内容就看重写的equals方法比较的是什么)
3.HashSet扩容机制有两个,一个是当添加的元素到达当前的扩容阈值(table数组总大小*0.75)时就会扩容,另一个是当数组还没有扩容到64之前,如果table数组下的某一条链表触发了树化函数,树化函数在树化操作之前就有一个扩容操作(直到table大小到达64才会树化)

树化机制
如果一个链表的元素个数到达默认阈值(8个),并且table表的大小到达默认最小树化容量(64个)就会将链表转换为红黑树
注意!!!:默认阈值不代表链表只能放八个节点,而是加入节点后,如果当前table表大小已经是64,且当前链表的数目超过8个就进行树化,如果加入节点(链表的第九个节点)后链表的大小还没到达64,就只会执行一次扩容,注意节点也是加入了当前链表的(当前链表有8个节点),直到下次有对象再加入当前链表才会再次触发树化的判断机制(即加入第10个节点)

注意!!!:前面所说的table大小是指整个table的大小,每次加入一个对象到table里,size就+1,这里的size可不是指table大小,通过树化进行判定的table扩容不需要看size,也就是不一定要全部位置拉满才扩容(扩容的是整个table的大小)
HashSet的add底层执行
第一次add “Java”(执行的语句都会标有带数字的注释)

一.先执行构造器HashSet()
public HashSet() {
 map = new HashMap<>(); 
}

二.执行add
public boolean add(E e) {//e = "java"
 return map.put(e, PRESENT)==null;//(static) PRESENT = new Object(); 
}
这里调用的还是map的put方法,e是传进来的需要存储的对象,PRESENT是map类的一个占位对象,因为map是键值对的形式,而set是一个值的,所以还需要用一个对象来占位,这样才能使用HashMap的方法

三.执行 put
public V put(K key, V value) {//key = "java" value = PRESENT 共享 
return putVal(hash(key), key, value, false, true); 
}
该方法会执行 hash(key) 得到 key 对应的 hash 值 
hash(key)的返回是h = key.hashCode()) ^(h >>> 16)【不是真正的hash值】 
//带数字的注释就是第一次add所执行的内容
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;//1.定义辅助变量
        if ((tab = table) == null || (n = tab.length) == 0)//2.if( (tab = table) == null ) --->tab = table; if(tab == null) 
        //表示table为空就第一次扩容到16个空间
            n = (tab = resize()).length;//3.resize会返回一个newtable给到tab,上一句的table也开辟了16个容量的数组,n是记录当前生成数组的长度
        if ((p = tab[i = (n - 1) & hash]) == null)//4.计算当前hash对应的table索引,并将找到的位置对应的tab[i]传给p,判断p是否为空
        //一开始table表是空的,tab[i]自然就是空的,将key(待存储的值),value(占位的),null表示当前节点的后面节点为空,当前待存储的值对应的hash值构建节点,存放到tab[i]
            tab[i] = newNode(hash, key, value, null);//5.
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//6.修改次数加1
        if (++size > threshold)//7.判断当前数据大小是否大于门槛,大就要进行一次扩容
            resize();
        afterNodeInsertion(evict);//对于hashmap来说这个方法为空,是给子类实现的
        return null;//8.返回null表示add成功,如果返回其他的值就表示hash已存在,即加入的对象重复
    }
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//一开始table是空的
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//获取到就容量为0
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1;
        }
        else if (oldThr > 0)
            newCap = oldThr;
        else {//oldCap=0,执行这段             
            newCap = DEFAULT_INITIAL_CAPACITY;//给了一个默认的初始容量 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//设置一个初始的门槛,意思就是到这个门槛就会执行扩容 12
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;//记录当前门槛 12
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//按照容量16初始化数组
        table = newTab;//将创建的数组赋给table
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;//最终返回
    }

第二次add “PHP”(前面的函数跳过,直接看putVal,执行的语句都会标有带数字的注释)

//带数字的注释就是第二次add所执行的内容
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;//1.定义辅助变量
        if ((tab = table) == null || (n = tab.length) == 0)//2.判断当前table表是否为空,不为空,
        //注意判断条件里面的tab = table和n = tab.length还是需要执行的,即依然将table赋给tab,n=16
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//3.计算当前hash(两次添加的对象不同)对应的table索引,并将找到的位置对应的tab[i]传给p,判断p是否为空
        //tab[i]空,将key(待存储的值),value(占位的),null表示当前节点的后面节点为空,当前待存储的值对应的hash值构建节点,存放到tab[i]
            tab[i] = newNode(hash, key, value, null);//4.
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//5.修改次数加1
        if (++size > threshold)//6.判断当前数据大小是否大于门槛,大就要进行一次扩容
            resize();
        afterNodeInsertion(evict);//对于hashmap来说这个方法为空,是给子类实现的
        return null;//7.返回null表示add成功,如果返回其他的值就表示hash已存在,即加入的对象重复
    }

第三次add “Java”(和第一次add的对象相同,前面的函数跳过,直接看putVal,执行的语句都会标有带数字的注释)

//带数字的注释就是第二次add所执行的内容
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;//1.定义辅助变量
        if ((tab = table) == null || (n = tab.length) == 0)//2.判断当前table表是否为空,不为空,
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//3.计算当前hash(和第一次添加的对象相同)对应的table索引,并将找到的位置对应的tab[i]传给p,判断p是否为空,不为空
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;//4.定义辅助变量
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//5.
                //注意这里的p所指向的是当前hash所对应i的table[i]下的链表第一个节点,
                //判断条件结构是(* && *)|| *
                //(* && *)表示比较当前p指向的对象所计算的hash值(数值)和当前传入对象的hash值进行比较,且同时满足当前p指向的对象和传入对象是同一个地址(即同一个对象,因为key是引用对象,用==比较就是比较地址,而不是比较内容)
                //|| 右边的*表示传入的对象不是同一个地址(即不同对象),但是p指向的对象的内容(这里用k是因为前一句条件就执行了k = p.key)和当前传入对象的内容相同
                e = p;//6.这里过后e就不为空了,表示失败
            else if (p instanceof TreeNode)
            //经过上条if判断后,走到这条程序就表示当前p指向的链表的第一个节点的对象所对应hash一定不与当前传入对象的hash相同,这里就判断p是否是红黑树节点,是就按红黑树的添加方式进行对象添加
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
             //这里走的是链表的添加方式,经过上条if判断后,走到这条程序就表示当前p指向的链表的第一个节点的对象所对应hash一定不与当前传入对象的hash相同,但是也只是确保了第一个节点对象和传入对象不同,链表后面的对象也要经过上面相同的判断,确保不会添加重复的对象或者不同对象但重复内容
                for (int binCount = 0; ; ++binCount) {//这个是死循环
                    if ((e = p.next) == null) {
                    //这条if表示是最后添加节点,比较得看后面的那个if,注意判断条件里执行了e = p.next,e永远执行p的后一位,最终e是指向空的
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) //这里就是判断节点添加链表后,立刻判断当前链表是否有8个节点
                            treeifyBin(tab, hash);//对执行树化函数,注意树化函数不是马上进行转红黑树的
                            //treeifyBin里面还会判断当前数组大小是否大于64,还没到64就会扩容table,其实就是尝试通过扩容的方式提高一点树化的门槛,因为用树存储会浪费空间
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;//如果出现重复内容,直接退出当前链表遍历,注意e=p.next(上面的判断语句每次循环都执行过)
                    p = e;//不满足上面的内容重复判断条件,将p后移一位,即p = p.next,如果上面有不满足直接break了,e就不指向空
                }
            }
            if (e != null) { //7.
            //只要将传入节点成功添加到table,e一定是指向null,不为空就说明要么传入同一个对象,要么传入不同对象但值相同,导致程序中途“断了”
                V oldValue = e.value;//8.value是那个占位的对象
                if (!onlyIfAbsent || oldValue == null)//9.这里的条件暂时不知道是什么
                    e.value = value;//10.
                afterNodeAccess(e);//11.这个函数暂时不清楚是什么
                return oldValue;//12.返回当前e的value,就是那个占位对象
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
HashSet的实现子类LinkedHashSet
  • LinkedHashSet底层是一个LinkedHashMap,底层维护的是数组+双向链表

  • LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,使得元素看上去以插入顺序一样存储

  • 第一次扩容的容量是16在这里插入图片描述
    在这里插入图片描述
    由上图可知,table的类型是HashMap$Node,而实际存放的节点类型是LinkedHashMap$Entry,这是多态的体现(子类对象存到父类的类型),LinkedHashMap$Entry是继承HashMap$Node实现的

  • Java的Entry是一个静态内部类,它实现了Map.Entry<K,V>这个接口,通过entry类可以构成一个单向链表,Entry将键值对的对应关系封装成了对象;

  • Map.Entry是Map接口的一个内部接口,这个内部接口表示Map的一个实体(key-value键值对),内部有getKey和getValue的方法;

  • ListHashMap里面还有一个叫做Entry的类,注意不要混淆了Map.Entry这个接口

Set 接口实现类-TreeSet

Map的接口和常用方法

  • Map 用于保存具有映射关系的数据:Key-Value(双列元素)
  • Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
  • Map 中的 key 不允许重复,原因和 HashSet 一样,Map 中的 value 可以重复
  • Map 的 key 可以为 null, value 也可以为 null ,注意 key 为 null只能有一个,value 为 null ,可以多个,常用 String 类作为 Map 的 key
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value
  • Node里面有hash,key,value,node.next四个部分组成

个人此时的理解(2022.11.20):
实际上Key-value是放在HashMap$Node这个类型里面,而Set和Collection只是分别指向Key和value(只是建立了一个引用,并没有重新建一份数据),是为了获取方便程序员遍历,才提供一组Set和Collection,并将这组东西打包成Entry,再把Entry放到entrySet方法里面?
entrySet是方法,EntrySet是集合(存放Entry类型的节点),两个东西是不同的
Node实现了Entry接口,把Node放在Entry类型的entrySet的集合是可以的(多态)
Set里面存放的是Entry<K,V>,但是Entry<K,V>是一个接口类型,如果你要通过它来调用数据,必须要实例化,Node正好实现了这个接口
引入EntrySet就是只是为了遍历Map方便

**(2022.11.21)**总之就是真正的key-value键值对具体对象是在之前的Node内部类里面的,而如果我们想要遍历的话,就可以通过获取keySet或者entrySet这两个集合(可以理解成具体对象的快捷方式,存的是地址)来获取对象信息,而其中通过keySet或者entrySet得到的set集合转型的原因涉及到泛型,在定义Set时如果么有明确给出泛型的具体类型时,keySet中的key和entrySet中的entry就默认是Object,如果想要通过keySet或者entrySet来获取key和value,就要将编译类型为Object的keySet或者entrySet向下转型为Map.Entry,才能调用属于Map.Entry的get、set方法
Map遍历元素的方式:
1.使用entrySet遍历

        Set entries = map.entrySet();
        for (Object entry:entries){
            //将 entry 转成 Map.Entry
			Map.Entry m = (Map.Entry) entry;
			System.out.println(m.getKey() + "-" + m.getValue());
        }

2.使用entrySet+Iterator遍历:这种方式需要使用entrySet方法加上迭代器

	Iterator iterator3 = entrySet.iterator();//注意放在获取entrySet之后
	while (iterator3.hasNext()) {
		Object entry = iterator3.next();
		//System.out.println(next.getClass());//HashMap$Node -实现-> Map.Entry (getKey,getValue)
		//向下转型 Map.Entry
		Map.Entry m = (Map.Entry) entry;
		System.out.println(m.getKey() + "-" + m.getValue());
	}

3.使用keySet/values遍历:可以选择使用for循环、增强for循环、do-while循环、lamdba表达式等方式对KeySet实现遍历;

System.out.println("通过map.keyset进行遍历key和value");
        for (Object key:map.keySet()){
            System.out.println("key=  "+key+"   and value=  "+map.get(key));
        }

4.使用keySet/values+Iterator遍历:这种方式需要使用keySet/values加上迭代器

Set keyset = map.keySet();
Iterator iterator = keyset.iterator();
while (iterator.hasNext()) {
	Object key = iterator.next();
	System.out.println(key + "-" + map.get(key));
}

Collection values = map.values();
Iterator iterator2 = values.iterator();
while (iterator2.hasNext()) {
	Object value = iterator2.next();
	System.out.println(value);
}
import java.util.*;

public class myCode {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        /**
         * 使用 HashMap 添加 3 个员工对象,要求
         * 键:员工 id
         * 值:员工对象
         *
         * 并遍历显示工资>18000 的员工(遍历方式最少两种)
         * 员工类:姓名、工资、员工 id
         */
        Map hsahMap = new HashMap();
        hsahMap.put(1, new Emp("老三", 6000, 1));
        hsahMap.put(2, new Emp("老里", 10000, 2));
        hsahMap.put(3, new Emp("老t", 5000, 3));

        //1.keySet+增强for
        Set keySet = hsahMap.keySet();//获取key的集合(集合里保存的是key对象的地址,并不是具体的key对象)
        for (Object key : keySet) {
            //先通过key获取
            Emp emp = (Emp) hsahMap.get(key);
            //注意hashMap里存放的key、value都是object类型,为什么呢?
            //因为上面泛型的原因,其实上面 Map hsahMap = new HashMap();就默认是Map<Object, Object> = new HashMap()
            //即key-values都默认是以Object类型进行存储的,所以这里要向下转型,不然就会报错
            if(emp.getSal() >8000) {
                System.out.println(emp);
            }
        }

        //2.entrySet+Iterator
        Set entrySet = hsahMap.entrySet();//先获取这个Map的entrySet,里面存储的也是key和value具体对象的地址
            Iterator iterator = entrySet.iterator();//获取集合的迭代器
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry)iterator.next();
            //转型的原因:因为entrySet里面的entry都是以Object类型进行存储的,所以不转型是用不了entry接口里面的方法的

            //通过 entry 取得 key 和 value
            Emp emp = (Emp) entry.getValue();
            //转型的原因:因为entry里面的key、value都是以Object类型进行存储的
            if(emp.getSal() > 8000) {
                System.out.println(emp);
            }
        }
    }
}
class Emp {
    private String name;
    private double sal;
    private int id;

    public Emp(String name, double sal, int id) {
        this.name = name;
        this.sal = sal;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getSal() {
        return sal;
    }

    public void setSal(double sal) {
        this.sal = sal;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Emp{" +
                "name='" + name + '\'' +
                ", sal=" + sal +
                ", id=" + id +
                '}';
    }
}

Map接口下的实现子类HashMap(HashSet的底层就是HashMap)

简单小结:
1.HashMap是以key-val对的方式来存储数据(HashMap$Node类型)
2.key不能重复,但是值可以重复,允许使用null键和null值
3.如果添加相同的key,则会覆盖原来的key-val,等同于修改value
4.与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式进行存储的
5.HashMap没有实现同步,因此是线程不安全的

Map接口下的实现子类Hashtable

底层;
1.底层有数组Hashtable/$Entry[] 初始化大小为11
(!!!注意这里的Entry不是之前的Map.Entry接口,这里是个类,还有就是这里没有Node这个东西了,可以理解成Entry替代了Node?)
2.扩容机制:2n+1(没有树化)

小结:
1.以key-val对的形式存储数据
2.键、值都不能为null
3.线程安全的(所以效率不如HashMap)
4.其他和HashMap的使用方法一致

Map接口下的继承Hashtable的实现子类-Properties(properties文件)

如果选择集合实现类
1.先判断存储的类型(一组对象【单列】或一组键值对【双列】)
2.一组对象(单列)

允许重复(List)-
增删多ListedList[底层维护一个双向链表]
改查多ArrayList[底层维护Object类型的可变数组]
不允许重复(Set)-
无序HashSet[底层是HashMap,维护一个hash表(数组+链表+红黑树)]
排序TreeSet
插入和取出顺序一致ListedHashSet[底层是ListedHashMap],维护数组+双向链表

3.一组键值对(双列)

--
键无序HashMap[底层是hash表(数组+链表+红黑树)]
键排序TreeMap
键插入和取出顺序一致ListedHashMap[底层是HashMap]
读取文件Properties
Map接口下的实现子类TreeMap

主要是看排序规则构建,一般用匿名内部类作为参数,即TreeMap treeMap = new TreeMap(-)的 “-"位置的参数
注意!!!:当比较器所得到的结果是当前键值对和某个已有的键值对相同(相同的判定条件是自定义的),则当前键值对是没有用的,不管是key还是value都不会加进去TreeMap之中

泛型

使用传统方法的问题:1.不能对加入到集合ArrayList中的数据类型进行约束;2.遍历时需要进行类型转换,影响效率

public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();//ArrayList的本意是建一个只存狗的集合,但没限制ArrayList的话,加入猫是可以的
        arrayList.add(new Dog("aa", 1));
        arrayList.add(new Dog("bb", 3));
        arrayList.add(new Dog("cc", 2));
        arrayList.add(new Cat("dd", 2));//加入了猫类对象,后面转型输出时会出现类型转换异常
        for (Object obj : arrayList) {
            Dog dog = (Dog) obj;//涉及向下转型,影响效率
            System.out.println(dog.getName() + " + " + dog.getAge());
        }
    }
public static void main(String[] args) {
        ArrayList<Dog> arrayList = new ArrayList();//约束可以加入arrayList的类型,提高安全性
        arrayList.add(new Dog("aa", 1));
        arrayList.add(new Dog("bb", 3));
        arrayList.add(new Dog("cc", 2));

        for (Dog obj : arrayList) {//减少类型转换的次数,提高效率
            System.out.println(obj.getName() + " + " + obj.getAge());
        }
    }
  • 泛型又称参数化类型,作用是可以在类声明时通过一个标识表示类中的某个属性的类型,或者某个属性的返回值类型,或者是参数类型,就是声明某些东西时用泛型E表示一个类型,等到具体用到这个东西时再给上具体的类型,然后所有的E就换成了具体的类型(这个时机是编译时),好处就是具体用时可以给任意类型

  • 泛型的声明:interface 接口<T>和 class 类<K,V>{} (注:K,V,T表示的都是类型,且只能放引用类型),可以<A,B,C,…>很多个泛型类型

  • 泛型标识符可以有多个

  • 普通成员可以使用泛型 (属性、方法)

  • 使用泛型的数组,不能初始化

  • 静态方法中不能使用类的泛型(如果静态方法和静态属性使用了泛型,JVM 就无法完成初始化)

  • 接口中,静态成员也不能使用泛型

  • 泛型接口的类型, 在继承接口或者实现接口时确定,没有指定类型,默认为 Object

  • 泛型不具备继承性

  • <?> :可以接受任意的泛型类型
  • <? extends AA> : 表示上限,可以接受 AA 或者 AA 子类
  • <? super AA> : 支持 AA 类以及 AA 类的父类,不限于直接父类
List<Object> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<AA> list3 = new ArrayList<>();
List<BB> list4 = new ArrayList<>();
List<CC> list5 = new ArrayList<>();

class AA {}
class BB extends AA {}
class CC extends BB {}

//表示AA类和AA类的子类可以调用这个方法,即list3、list4、list5
public static void printCollection2(List<? extends AA> c) {
	for (Object object : c) {
		System.out.println(object);
	}
}
//表示所有的泛型类可以调用这个方法,即list1、list2、list3、list4、list5
public static void printCollection1(List<?> c) {
	for (Object object : c) {
		System.out.println(object);
	}
}
//表示AA类和AA类的父类可以调用这个方法,即list1、list2、list3
public static void printCollection3(List<? super AA> c) {
	for (Object object : c) {
		System.out.println(object);
	}
}
/*题目:
定义泛型类DAO<T>,在其中定义一个Map成员变量,Map的键为String类型,值为T类型
分别创建以下方法:
(1)public void save(String id, T entity):保存T类型的对象
(2)public T get(String id) :从map中获取id对应的对象
(3)public void update(String id, T entity):替换map中key为id的内容,改为value对象
(4)public List<T> list() :返回map中存放的所有T对象
(5)public void delete(String id):删除指定id对象
定义一个User类(id age name),创建DAO对象使用创建的方法操作User对象(使用Junit单元测试类进行测试)
*/
import org.junit.jupiter.api.Test;

import java.util.*;

public class myCode {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
		//使用Junit进行测试,注意如果输入 “@Test”爆红时就是还没有引入Junit
    }
    @Test
    public void testList() {
        DAO<User> dao = new DAO<>();
        dao.save("001", new User(1, 23, "萨达"));
        dao.save("002", new User(2, 25, "蜂窝"));
        dao.save("004", new User(4, 35, "马鞍山"));
        dao.update("004", new User(4, 35, "马鞍山666"));
        dao.delete("001");
        System.out.println(dao.get("002"));
        System.out.println(dao.list());
    }
}
class DAO<T> {
    private Map<String, T> map = new HashMap<>();

    public void save(String id, T entity) {
        map.put(id, entity);
    }

    public T get(String id) {
        return map.get(id);
    }

    public void update(String id, T entity) {
        map.put(id, entity);
    }

    public List<T> list() {
        List<T> list = new ArrayList<>();
        for (Map.Entry<String, T> entry: map.entrySet()) {
            list.add(entry.getValue());
        }
        return list;
    }

    public void delete(String id) {
        map.remove(id);
    }
}
class User {
    private int id;
    private int age;
    private String name;

    public User(int id, int age, String name) {
        this.id = id;
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜以冀北

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值