Java数据结构-Map和Set-通配符?-反射-枚举-Lambda
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 1. Map和Set
- 1.1 二叉搜索树
- 1.2 Map
- 1.3 Set
- 1.4 哈希表
- 2. 通配符?
- 3. 反射
- 4. 枚举
- 5. Lambda表达式
- 5.1 函数式接口
- 5.2 变量捕获
- 5.3 Lambda在集合当中的使用
- 5.3 List接口
- 5.4 Map接口
- 总结
前言

1. Map和Set
1.1 二叉搜索树
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树


这是二叉树的查找

代码也很简单

插入也很简单,直接就插入到叶子节点了

先找到叶子节点,然后就可以插入了
难点是二叉搜索树的删除了
设待删除结点为 cur, 待删除结点的双亲结点为 parent
cur.left == nullcur 是 root,则 root = cur.rightcur 不是 root,cur 是 parent.left,则 parent.left = cur.rightcur 不是 root,cur 是 parent.right,则 parent.right = cur.right
cur.right == nullcur 是 root,则 root = cur.leftcur 不是 root,cur 是 parent.left,则 parent.left = cur.leftcur 不是 root,cur 是 parent.right,则 parent.right = cur.left
cur.left != null && cur.right != null
1. 需要使用替换法进行删除,即在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题
先找到这个节点,然后进行删除

TreeSet和TreeMap底层是用的红黑树
Map和set是一种专门用来进行搜索的容器或者数据结构
一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型会有两种:
-
纯 key 模型,比如:
有一个英文词典,快速查找一个单词是否在词典中
快速查找某个名字在不在通讯录中 -
Key-Value 模型,比如:
统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>
梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
而Map中存储的就是key-value的键值对,Set中只存储了Key。
Set就是纯 key 模型
Map就是Key-Value 模型
1.2 Map
Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复。
Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了<key, value>的获取,value的设置以及Key的比较方式
V get(Object key) 返回 key 对应的 value
V getOrDefault(Object key, V defaultValue) 返回 key 对应的 value,key 不存在,返回默认值
V put(K key, V value) 设置 key 对应的 value
V remove(Object key) 删除 key 对应的映射关系
Set<K> keySet() 返回所有 key 的不重复集合
Collection<V> values() 返回所有 value 的可重复集合
Set<Map.Entry<K, V>> entrySet() 返回所有的 key-value 映射关系
boolean containsKey(Object key) 判断是否包含 key
boolean containsValue(Object value) 判断是否包含 value
Map<String,String> map= new TreeMap<>();map.put("a","1");map.put("b","2");map.put("c","3");String val = map.get("c");System.out.println(val);

String orDefault = map.getOrDefault("aaa", "aaa1");System.out.println(orDefault);
这个意思就是取key为aaa的value,如果没有,就取默认值aaa1
String a = map.remove("a");System.out.println(a);
Set<String> stringSet = map.keySet();System.out.println(stringSet);

map.put("a","1");map.put("a","2");
key重复了就会更新value值
Set<Map.Entry<String, String>> entries = map.entrySet();System.out.println(entries);

for (Map.Entry<String, String> entry : entries) {System.out.println(entry.getKey());System.out.println(entry.getValue());}

- Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
- Map中存放键值对的Key是唯一的,value是可以重复的
- 在TreeMap中插入键值对时,key不能为空,否则就会抛NullPointerException异常,value可以为空。但是HashMap的key和value都可以为空。
- Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
- Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
- Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入

注意,往TreeMap里面存入的key必须可以比较,不然怎么进行搜索
HashMap:哈希桶。O(1)
1.3 Set
Set<String> stringSet = new TreeSet<>();stringSet.add("a");stringSet.add("b");stringSet.add("a");System.out.println(stringSet);

Set中不能存储重复元素
boolean add(E e) 添加元素,但重复元素不会被添加成功
void clear() 清空集合
boolean contains(Object o) 判断 o 是否在集合中
Iterator<E> iterator() 返回迭代器
boolean remove(Object o) 删除集合中的 o
int size() 返回set中元素的个数
boolean isEmpty() 检测set是否为空,空返回true,否则返回false
Object[] toArray() 将set中的元素转换为数组返回
boolean containsAll(Collection<?> c) 集合c中的元素是否在set中全部存在,是返回true,否则返回false
boolean addAll(Collection<? extends E> c)将集合c中的元素添加到set中,可以达到去重的效果
Iterator<String> iterator = stringSet.iterator();while (iterator.hasNext()) {System.out.println(iterator.next());}
1. Set是继承自Collection的一个接口类
2. Set中只存储了key,并且要求key一定要唯一
3. TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的
4. Set最大的功能就是对集合中的元素进行去重
5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。
6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入
7. TreeSet中不能插入null的key,HashSet可以。

注意TreeSet底层就是TreeMap
每次add,value都是Object对象,key唯一
1.4 哈希表
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)
不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”
由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率
避免哈希冲突:1.设计合理的哈希函数,2. 降低负载因子



所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去
-
线性探测
比如上面的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。 -
二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。
- HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
- java 中使用的是哈希桶方式解决冲突的
- java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
- java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。



所以打印false

所以打印出True
就是把s1指向的对象放入常量池中,常量池中没有这个对象,放入,有的话,就不用放了
2. 通配符?


就是不知道传给fun的Message是什么类型的,就可以用?
?就是通配符
?extends类:通配符的上界
?super类:通配符的下界

下界就是表示传入为number或者number的父类

父类变子类,只能强制类型转换
3. 反射
Java的反射(reflection)机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,既然能拿到那么,我们就可以修改部分类型信息;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射(reflection)机制
Java程序中许多对象在运行时会出现两种类型:运行时类型(RTTI)和编译时类型,例如Person p = newStudent();这句代码中p在编译时类型为Person,运行时类型为Student。程序需要在运行时发现对象和类的真实信息。而通过使用反射程序就能判断出该对象和类属于哪些类
反射相关的类
Class类 代表类的实体,在运行的Java应用程序中表示类和接口
Field类 代表类的成员变量/类的属性
Method类 代表类的方法
Constructor类 代表类的构造方法
Java文件被编译后,生成了.class文件,JVM此时就要去解读.class文件 ,被编译后的Java文件.class也被JVM解析为一个对象,这个对象就是 java.lang.Class .这样当程序在运行时,每个java文件就最终变成了Class类对象的一个实例。我们通过Java的反射机制应用到这个实例,就可以去获得甚至去添加改变这个类的属性和动作,使得这个类成为一个动态的类
反射就是一个照妖镜,可以看到真实的内容
获得Class对象的三种方式
在反射之前,我们需要做的第一步就是先拿到当前需要反射的类的Class对象,然后通过Class对象的核心方法,达到反射的目的,即:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,既然能拿到那么,我们就可以修改部分类型信息。
第一种,使用 Class.forName(“类的全路径名”); 静态方法。
前提:已明确类的全路径名。
第二种,使用 .class 方法。
说明:仅适合在编译前就已经明确要操作的 Class
第三种,使用类对象的 getClass() 方法
class Student{//私有属性nameprivate String name = "bit";//公有属性agepublic int age = 18;//不带参数的构造方法public Student(){System.out.println("Student()");}private Student(String name,int age) {this.name = name;this.age = age;System.out.println("Student(String,name)");}private void eat(){System.out.println("i am eat");}public void sleep(){System.out.println("i am pig");}private void function(String str) {System.out.println(str);}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}
}
Class<?> c1= null;try {c1 = Class.forName("com.ck.demo.shujujiegou.Student");} catch (ClassNotFoundException e) {throw new RuntimeException(e);}
Class<Student> studentClass = Student.class;Class<?> studentClass2 = Student.class;
Student student = new Student();Class<?> aClass = student.getClass();
这三个都是同一个Class对象
地址都是一样的
Class对象只有一个
一个类在一个Jvm中只有一个Class对象
怎么使用呢
Class<?> c1= null;try {c1 = Class.forName("com.ck.demo.shujujiegou.Student");Student student = (Student) c1.newInstance();} catch (ClassNotFoundException e) {throw new RuntimeException(e);} catch (InstantiationException e) {throw new RuntimeException(e);} catch (IllegalAccessException e) {throw new RuntimeException(e);}
这个是用Class对象创建对象

c1 = Class.forName("com.ck.demo.shujujiegou.Student");Student student = (Student) c1.newInstance();Constructor<Student> constructor = (Constructor<Student>)c1.getDeclaredConstructor(String.class, int.class);constructor.setAccessible(true);Student student1 = constructor.newInstance("nihao", 13);System.out.println(student1);
这个就是创建Studednt私有的构造方法
然后创建Student
constructor.setAccessible(true);的作用就是让私有不生效了

这样我们就通过反射调用私有方法了
c1 = Class.forName("com.ck.demo.shujujiegou.Student");Constructor<Student> constructor = (Constructor<Student>)c1.getDeclaredConstructor();Student student1 = constructor.newInstance();Field name = c1.getDeclaredField("name");//获取name字段name.setAccessible(true);//设置私有也可用name.set(student1, "zhangsan");//给student1设置name字段System.out.println(student1);

这样的话,就算是私有的成员,仿射也是可以访问到的
c1 = Class.forName("com.ck.demo.shujujiegou.Student");Constructor<Student> constructor = (Constructor<Student>)c1.getDeclaredConstructor();Student student1 = constructor.newInstance();Method method = c1.getDeclaredMethod("function",String.class);//获取方法,表示function方法的参数类型是Stringmethod.setAccessible(true);method.invoke(student1,"我是方法参数");

这样就调用到了私有方法了
优点:
- 对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法
- 增加程序的灵活性和扩展性,降低耦合性,提高自适应能力
- 反射已经运用在了很多流行框架如:Struts、Hibernate、Spring 等等。
缺点: - 使用反射会有效率问题。会导致程序效率降低。具体参考这里:http://www.imooc.com/article/293679
- 反射技术绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂 。
4. 枚举
enum TestEnum{RED,BLUE,YELLOW;
}
TestEnum testEnum = TestEnum.RED;switch (testEnum){case RED:System.out.println("RED");break;case BLUE:System.out.println("BLUE");break;case YELLOW:System.out.println("YELLOW");break;}
values() 以数组形式返回枚举类型的所有成员
ordinal() 获取枚举成员的索引位置
valueOf() 将普通字符串转换为枚举实例
compareTo() 比较两个枚举成员在定义时的顺序
TestEnum[] values = TestEnum.values();for (TestEnum testEnum : values) {System.out.println(testEnum);System.out.println(testEnum.ordinal());}

TestEnum red = TestEnum.valueOf("RED");System.out.println(red);

int i = RED.compareTo(YELLOW);System.out.println(i);

当定义好一个枚举类之后,,就会默认继承一个Enum的类,所以就会有这些方法

Enum是一个抽象类
重要:枚举的构造方法默认是私有的

enum TestEnum{RED("红色",1),BLUE("蓝色",2),YELLOW("黄色",3);public int value;public String name;TestEnum(String name,int value){this.value = value;this.name = name;}
}
RED(“红色”,1)当使用这个的时候,说明是带参数的构造方法了,那么就不能使用以前的编译器默认的不带参数的构造方法了
enum TestEnum{RED("红色",1),BLUE("蓝色",2),YELLOW("黄色",3);public int value;public String name;TestEnum(String name,int value){this.value = value;this.name = name;}
}
怎么通过反射,获取到枚举的私有的构造方法呢
c1 = Class.forName("com.ck.demo.shujujiegou.TestEnum");Constructor<TestEnum> constructor = (Constructor<TestEnum>)c1.getDeclaredConstructor(String.class,int.class);constructor.setAccessible(true);TestEnum testEnum = constructor.newInstance(19,"白色");System.out.println(testEnum);

但是直接报错了,这个是因为枚举在反射的时候,要传四个参数,多余的两个参数是传给父类的,而且写在前面
c1 = Class.forName("com.ck.demo.shujujiegou.TestEnum");Constructor<TestEnum> constructor = (Constructor<TestEnum>)c1.getDeclaredConstructor(String.class,int.class,String.class,int.class);constructor.setAccessible(true);TestEnum testEnum = constructor.newInstance(19,"白色");System.out.println(testEnum);

为什么还会报错呢

当你使用反射来创建枚举的时候—》编译器不允许这样做
原版问题是:为什么枚举实现单例模式是安全的?
1、枚举本身就是一个类,其构造方法默认为私有的,且都是默认继承与 java.lang.Enum
2、枚举可以避免反射和序列化问题
3、枚举的优点和缺点
5. Lambda表达式
基本语法: (parameters) -> expression 或 (parameters) ->{ statements; }
Lambda表达式由三部分组成:
- paramaters:类似方法中的形参列表,这里的参数是函数式接口里的参数。这里的参数类型可以明确的声明也可不声明而由JVM隐含的推断。另外当只有一个推断类型时可以省略掉圆括号。
- ->:可理解为“被用于”的意思
- 方法体:可以是表达式也可以代码块,是函数式接口里方法的实现。代码块可返回一个值或者什么都不反回,这里的代码块块等同于方法的方法体。如果是表达式,也可以返回一个值或者什么都不反回。
// 1. 不需要参数,返回值为 2
() -> 2
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的和
(x, y) -> x + y
// 4. 接收2个int型整数,返回他们的乘积
(int x, int y) -> x * y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
5.1 函数式接口
函数式接口定义:一个接口有且只有一个抽象方法 。
注意:
- 如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口
- 如果我们在某个接口上声明了 @FunctionalInterface 注解,那么编译器就会按照函数式接口的定义来要求该接口,这样如果有两个抽象方法,程序编译就会报错的。所以,从某种意义上来说,只要你保证你的接口中只有一个抽象方法,你可以不加这个注解。加上就会自动进行检测的。

@FunctionalInterface
interface test10{void test();
}
@FunctionalInterface
interface NoParameterNoReturn {void test();default void test2() {System.out.println("JDK1.8新特性,default默认方法可以有具体的实现");}
}
这样也是可以的,因为default 不是抽象方法
或者static修饰也可以的
//无返回值无参数
@FunctionalInterface
interface NoParameterNoReturn {void test();
}
//无返回值一个参数
@FunctionalInterface
interface OneParameterNoReturn {void test(int a);
}
//无返回值多个参数
@FunctionalInterface
interface MoreParameterNoReturn {void test(int a,int b);
}
//有返回值无参数
@FunctionalInterface
interface NoParameterReturn {int test();
}
//有返回值一个参数
@FunctionalInterface
interface OneParameterReturn {int test(int a);
}
//有返回值多参数
@FunctionalInterface
interface MoreParameterReturn {int test(int a,int b);
}
然后开始使用上面的函数式接口
NoParameterReturn noParameterReturn = new NoParameterReturn() {@Overridepublic int test() {System.out.println("noParameterReturn");return 0;}};
这是没有使用Lambda表达式的时候
NoParameterNoReturn noParameterNoReturn = ()->{System.out.println("noParameterReturn");};noParameterNoReturn.test();
NoParameterNoReturn noParameterNoReturn = ()-> System.out.println("noParameterReturn");;noParameterNoReturn.test();
OneParameterNoReturn oneParameterNoReturn = (a)->{System.out.println(a);};OneParameterNoReturn oneParameterNoReturn2 = a->{System.out.println(a);};oneParameterNoReturn.test(10);
MoreParameterNoReturn moreParameterNoReturn = (a,b)->{System.out.println(a+b);};moreParameterNoReturn.test(1,2);
MoreParameterReturn moreParameterReturn = (a,b)->{return a+b;};MoreParameterReturn moreParameterReturn2 = (a,b)-> a+b;moreParameterReturn.test(1,2);
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return o1.compareTo(o2);}});
PriorityQueue<Integer> priorityQueue2 =new PriorityQueue<>((o1,o2)->{return o1.compareTo(o2);});
- 参数类型可以省略,如果需要省略,每个参数的类型都要省略。
- 参数的小括号里面只有一个参数,那么小括号可以省略
- 如果方法体当中只有一句代码,那么大括号可以省略
- 如果方法体中只有一条语句,且是return语句,那么大括号可以省略,且去掉return关键字。
5.2 变量捕获
int a = 10;PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {a=100;//这样不行return o1.compareTo(o2);}});
在匿名内部类中,捕获的变量只能是常量,不能修改的,只能访问
Lambda也是一样的


5.3 Lambda在集合当中的使用
为了能够让Lambda和Java的集合类集更好的一起使用,集合当中,也新增了部分接口,以便与Lambda表达式对接。
ArrayList<String> list = new ArrayList<>();list.add("Hello");list.add("hello");list.add("lambda");list.forEach();


foeeach参数Consumer是一个函数式接口
所以我们就可以重写Consumer方法
list.forEach(new Consumer<String>() {@Overridepublic void accept(String s) {System.out.println(s);}});
list.forEach(s->System.out.println(s));
这样都是可以的
5.3 List接口
list.sort(new Comparator<String>() {@Overridepublic int compare(String o1, String o2) {return 0;}});
Comparator也是一个函数式接口
list.sort((o1,o2)->{return o1.compareTo(o2);});
5.4 Map接口
HashMap<Integer, String> map = new HashMap<>();map.put(1, "hello");map.put(2, "bit");map.put(3, "hello");map.put(4, "lambda");map.forEach(new BiConsumer<Integer, String>() {@Overridepublic void accept(Integer integer, String s) {System.out.println(integer+s);}});
map.forEach((k,v)->System.out.println(k+v));
Lambda表达式的优点很明显,在代码层次上来说,使代码变得非常的简洁。缺点也很明显,代码不易读
优点:
- 代码简洁,开发迅速
- 方便函数式编程
- 非常容易进行并行计算
- Java 引入 Lambda,改善了集合操作
缺点: - 代码可读性变差
- 在非并行计算中,很多计算未必有传统的 for 性能要高
- 不容易进行调试
