Java基础知识学习:简单随手记录(3)
学习视频链接: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
- 泛型
一.枚举和注解
- 枚举是一组常量的集合。
- 枚举属于一种特殊的类,里面只包含一组有限的特定的对象。
自定义类实现枚举
- 构造器私有化
- 本类内部创建一组对象[四个 春夏秋冬]
- 对外暴露对象(通过为对象添加 public final static 修饰符)
- 可以提供 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{}
------------------------------------------------------------------------------------------------------------------------------------------
- 注解(Annotation)也被称为元数据(Metadata),用于修饰解释 包、类、方法、属性、构造器、局部变量等数据信息。
- 和注释一样,注解不影响程序逻辑,但注解可以被编译或运行,相当于嵌入在代码中的补充信息。
三个基本的 Annotation(待学习)
@Override: 限定某个方法,是重写父类方法, 该注解只能用于方法;
如果写了@Override 注解,编译器就会去检查该方法是否真的重写了父类的方法,如果的确重写了,则编译通过,如果没有构成重写,则编译错误@Deprecated: 用于表示某个程序元素(类, 方法等)已过时;
@SuppressWarnings: 抑制编译器警告;
二.异常
- NullPointerException 空指针异常
- ArithmeticException 数学运算异常
- ArrayIndexOutOfBoundsException 数组下标越界异常
- ClassCastException 类型转换异常
- 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)相关推荐
- JAVA基础知识学习全覆盖
文章目录 一.JAVA基础知识 1.一些基本概念 1.Stringbuffer 2.局部变量成员变量 3.反射机制 4.protect 5.pow(x,y) 6.final ,finally,fina ...
- java基础知识学习小总结(一)
此文转载自:https://blog.csdn.net/weixin_44734093/article/details/109715246 什么是java Java是一门面向对象编程语言,不仅吸收了C ...
- java 基础知识学习2
目录 目录 基础知识练习 String 类实现大小写转换的方法 截取字符串中的部分内容 用正则表达式判断手机号码是否合法 用字符串生成器追加字符 用连接运算符连接字符串 去除字符串中的首尾控格 获取字 ...
- Java基础知识学习笔记总结
Java学习笔记总结 java基础复习 1. 抽象类可以有构造器,可以有一个非抽象的父类 2. 垃圾回收机制回收的是堆里面的内存,栈里面的数据自动入栈自动出栈 3. 引用类型的数据在堆当中,内存中操作 ...
- Java基础知识学习:简单随手记录(1)
学习视频链接:https://www.bilibili.com/video/BV1fh411y7R8?p=1&vd_source=1635a55d1012e0ef6688b3652cefcdf ...
- 超详细的java基础知识学习(java SE、javaEE)笔记 核心重点!
标识符 Java 的标识符是由字母.数字.下划线_.以及美元符$组成,但是首字母不可以是数字.Java 标识符大小写敏感,长度无限制,不能是 Java 中的关键字.命名规则:要见名知意! u 变量要 ...
- Java基础知识学习(七)
线程(续) 线程同步 当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用.达到此目的的过程叫做同步(synchronization) 可以用两种方法同步化代码.两者 ...
- JAVA基础知识学习
1.各个方面知识 很全面的知识总结(推荐): https://www.yuque.com/crow/simpread/23aba84d-73b3-4950-9621-bf511b2d088a#cf65 ...
- Java基础知识学习巩固2--int和Integer有什么区别及扩展
这个问题之前首先要介绍下Java数据类型, 一.Java基本类型,主要有8种,分别是: 1.boolean(布尔型即只有true和false), 2.char(字节型16 位 Unicode 字符), ...
最新文章
- android单线字体,Android自定义字体
- 设计模式之Pimpl模式
- 大数据时代,如何根据业务选择合适的分布式框架
- Halcon 彩色图片通道分割处理
- C++ 之new和delete释放内存
- 文件不能断点 webstorm_详解python使用金山词霸的翻译功能(调试工具断点的使用)...
- Orbeon form PE 版本 dmv-14 点击 save 按钮之后的执行逻辑
- Nginx系列二:(Nginx Rewrite 规则、Nginx 防盗链、Nginx 动静分离、Nginx+keepalived 实现高可用)...
- GitLab 小组中的项目访问权限赋予给用户
- 【数字逻辑设计】Logisim构建多路选择器
- android 编译 sdl,SDL编译 - Android本地视频播放器开发_Linux编程_Linux公社-Linux系统门户网站...
- vb.net中递归退到最外层_面试题被问到再也不慌,深究JavaScript中的深拷贝与浅拷贝...
- 宝马冷却系统及电动冷却液泵部件(电子水泵)功能特性及标准
- TP6使用session
- setprecision、setw、fixed详解
- Python爬虫进阶--js逆向 | 某某云加速参数加密分析
- mysql水仙花数,水仙花数_水仙花数c语言程序
- 2022年宋干节活动-乌隆他尼皇家大学
- 玲珑杯Unity开发心得——进度条界面(异步加载游戏场景)
- 第四次面试----华数电力科技有限公司
热门文章
- Linux系统物理CPU、逻辑CPU和CPU核数的区别
- 作为小白接融云 IM SDK 新路体验~
- IDEA的导入Seckill项目后,启动出现404的解决办法
- profile参数详解
- 数据应用场景之标签管理体系
- tensorflow.python.framework.errors_impl.FailedPreconditionError: Could not find variable Variable.
- 换个角度看百度的渡鸦事件
- android ios wp三大平台神器软件分享
- 一篇文章讲清楚交叉熵和KL散度
- MATLAB图像处理入门