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

目录

  • 一.枚举和注解
    • 自定义类实现枚举
    • enum关键字实现枚举
    • enum 实现接口
    • 三个基本的 Annotation(待学习)
  • 二.异常
    • 异常处理机制
    • throws 异常处理
    • 自定义异常
    • try/catch-final执行顺序讨论:如果finally存在,任何执行try 或者catch中的return语句之前,都会先执行finally语句。如果finally中有return语句,那么程序就return了,所以finally中的return是一定会被return的。
  • 三.包装类
    • Integer
    • String(有很多构造器)
    • StringBuffer
    • StringBuild
  • 四.集合
    • Collection的接口和常用方法
      • Collection下的子接口List
        • ArrayList底层和源码结构
        • Vector底层结构
        • LinkedList底层结构(源码就是链表的操作)
      • Collection下的子接口Set
        • Set 接口实现类-HashSet
          • HashSet的实现子类LinkedHashSet
        • Set 接口实现类-TreeSet
    • Map的接口和常用方法
      • Map接口下的实现子类HashMap(HashSet的底层就是HashMap)
      • Map接口下的实现子类Hashtable
        • Map接口下的继承Hashtable的实现子类-Properties(properties文件)
      • Map接口下的实现子类TreeMap
  • 泛型

一.枚举和注解

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

自定义类实现枚举

  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;}@Overridepublic 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;}@Overridepublic 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=2String[] names = new String[3];//全为nullif (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=4System.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,如果是就执行finalthrow 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);//指向同一个对象地址,trueInteger 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指向的地址,trueSystem.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 1sttreeifyBin(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.修改次数加1if (++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;//获取到就容量为0int 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;//给了一个默认的初始容量 16newThr = (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;//将创建的数组赋给tableif (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 orderNode<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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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=16n = (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 1sttreeifyBin(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.修改次数加1if (++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.EntryMap.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.EntryMap.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+增强forSet 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+IteratorSet 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 和 valueEmp 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;}@Overridepublic 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}@Testpublic 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;}@Overridepublic String toString() {return "User{" +"id=" + id +", age=" + age +", name='" + name + '\'' +'}';}
}

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

  1. JAVA基础知识学习全覆盖

    文章目录 一.JAVA基础知识 1.一些基本概念 1.Stringbuffer 2.局部变量成员变量 3.反射机制 4.protect 5.pow(x,y) 6.final ,finally,fina ...

  2. java基础知识学习小总结(一)

    此文转载自:https://blog.csdn.net/weixin_44734093/article/details/109715246 什么是java Java是一门面向对象编程语言,不仅吸收了C ...

  3. java 基础知识学习2

    目录 目录 基础知识练习 String 类实现大小写转换的方法 截取字符串中的部分内容 用正则表达式判断手机号码是否合法 用字符串生成器追加字符 用连接运算符连接字符串 去除字符串中的首尾控格 获取字 ...

  4. Java基础知识学习笔记总结

    Java学习笔记总结 java基础复习 1. 抽象类可以有构造器,可以有一个非抽象的父类 2. 垃圾回收机制回收的是堆里面的内存,栈里面的数据自动入栈自动出栈 3. 引用类型的数据在堆当中,内存中操作 ...

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

    学习视频链接:https://www.bilibili.com/video/BV1fh411y7R8?p=1&vd_source=1635a55d1012e0ef6688b3652cefcdf ...

  6. 超详细的java基础知识学习(java SE、javaEE)笔记 核心重点!

    标识符 Java 的标识符是由字母.数字.下划线_.以及美元符$组成,但是首字母不可以是数字.Java 标识符大小写敏感,长度无限制,不能是 Java 中的关键字.命名规则:要见名知意! u  变量要 ...

  7. Java基础知识学习(七)

    线程(续) 线程同步 当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用.达到此目的的过程叫做同步(synchronization) 可以用两种方法同步化代码.两者 ...

  8. JAVA基础知识学习

    1.各个方面知识 很全面的知识总结(推荐): https://www.yuque.com/crow/simpread/23aba84d-73b3-4950-9621-bf511b2d088a#cf65 ...

  9. Java基础知识学习巩固2--int和Integer有什么区别及扩展

    这个问题之前首先要介绍下Java数据类型, 一.Java基本类型,主要有8种,分别是: 1.boolean(布尔型即只有true和false), 2.char(字节型16 位 Unicode 字符), ...

最新文章

  1. android单线字体,Android自定义字体
  2. 设计模式之Pimpl模式
  3. 大数据时代,如何根据业务选择合适的分布式框架
  4. Halcon 彩色图片通道分割处理
  5. C++ 之new和delete释放内存
  6. 文件不能断点 webstorm_详解python使用金山词霸的翻译功能(调试工具断点的使用)...
  7. Orbeon form PE 版本 dmv-14 点击 save 按钮之后的执行逻辑
  8. Nginx系列二:(Nginx Rewrite 规则、Nginx 防盗链、Nginx 动静分离、Nginx+keepalived 实现高可用)...
  9. GitLab 小组中的项目访问权限赋予给用户
  10. 【数字逻辑设计】Logisim构建多路选择器
  11. android 编译 sdl,SDL编译 - Android本地视频播放器开发_Linux编程_Linux公社-Linux系统门户网站...
  12. vb.net中递归退到最外层_面试题被问到再也不慌,深究JavaScript中的深拷贝与浅拷贝...
  13. 宝马冷却系统及电动冷却液泵部件(电子水泵)功能特性及标准
  14. TP6使用session
  15. setprecision、setw、fixed详解
  16. Python爬虫进阶--js逆向 | 某某云加速参数加密分析
  17. mysql水仙花数,水仙花数_水仙花数c语言程序
  18. 2022年宋干节活动-乌隆他尼皇家大学
  19. 玲珑杯Unity开发心得——进度条界面(异步加载游戏场景)
  20. 第四次面试----华数电力科技有限公司

热门文章

  1. Linux系统物理CPU、逻辑CPU和CPU核数的区别
  2. 作为小白接融云 IM SDK 新路体验~
  3. IDEA的导入Seckill项目后,启动出现404的解决办法
  4. profile参数详解
  5. 数据应用场景之标签管理体系
  6. tensorflow.python.framework.errors_impl.FailedPreconditionError: Could not find variable Variable.
  7. 换个角度看百度的渡鸦事件
  8. android ios wp三大平台神器软件分享
  9. 一篇文章讲清楚交叉熵和KL散度
  10. MATLAB图像处理入门