1、集合框架
1.1 、集合框架体系图
Java 的集合框架主要分为五大类体系:
1、Collection(常用的 List 和 Set,和不常用的 Queue 和 Vector 和 Stack),单元素集合
2、Map(常用的 HashMap 和 TreeMap,不常用的 HashTable),Key-Value 映射
3、Iterator(迭代器)
4、工具类(Collections 和 Arrays)
5、Comparable 和 Comparator 比较器
Java 中的集合和数组的区别:
1、数组长度在初始化时指定,意味着只能保存定长的数据。而集合可以保存数量不确定的 数据。同时可以保存具有映射关系的数据(即关联数组,键值对 key-value)。
2、数组元素即可以是基本类型的值,也可以是对象。集合里只能保存对象(实际上只是保存对象的引用变量),基本数据类型的变量要转换成对应的包装类才能放入集合类中。
Collection 接口中的方法:
Map 接口中的方法:
1.2、常用集合特性概述
1.2.1 List 系
List 特点:元素有放入顺序,元素可重复
List 接口有三个实现类:LinkedList,ArrayList,Vector
LinkedList:底层基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还 存储下一个元素的地址。链表增删快,查找慢
ArrayList 和 Vector 底层都是基于数组实现的,查询快,增删慢,区别是 ArrayList 是非线程安全的,效率高;Vector 是基于线程安全的,效率低
ArrayList 的初始化大小是 10,扩容策略是 1.5 倍原元素数量的大小
数组 初始容量+扩容 (jdk10)
1 | // 初始容量 |
选择标准:
如果涉及到“动态数组”、“栈”、“队列”、“链表”等结构,应该考虑用 List,具体的选择哪 个 List,根据下面的标准来取舍。
1、对于需要快速插入,删除元素,应该使用 LinkedList。(增删改)
2、对于需要快速随机访问元素,应该使用 ArrayList。(查询)
3、对于“单线程环境”或者“多线程环境,但 List 仅仅只会被单个线程操作”,此时应该使 用非同步的类(如 ArrayList)。对于“多线程环境,且 List 可能同时被多个线程操作”,此时, 应该使用同步的类(如 Vector)。
LinkedList add(E e)
1 | # add(E e) 源码 |
1.2.2、Set 系
Set 特点:元素放入无顺序,元素不可重复
Set 接口的实现类:HashSet,TreeSet,LinkedHashSet
HashSet(底层由 HashMap 实现)底层通过 hashCode()和 equals()进行去重。
HashSet 内部判断相等的标准
HashSet 判断两个元素相等的标准:
两个对象通过 equals()方法比较相等,并且两个对象的 hashCode()方法返回值也相等
HashSet 中判断集合元素相等,两个对象比较具体分为如下四个情况:
- 如果有两个元素通过 equal()方法比较返回 false,并且它们的 hashCode()方法返回不相等,
HashSet 将会把它们存储在不同的位置。
- 如果有两个元素通过 equal()方法比较返回 true,并且它们的 hashCode()方法返回不相等,
HashSet 将会把它们存储在不同的位置。
- 如果两个对象通过 equals()方法比较不相等,hashCode()方法比较相等,HashSet 将会把它们存储在相同的位置,在这个位置以链表式结构来保存多个对象。这是因为当向 HashSet 集合中存入一个元素时,HashSet 会调用对象的 hashCode()方法来得到对象的 hashCode 值, 然后根据该 hashCode 值来决定该对象存储在 HashSet 中存储位置。
- 如果有两个元素通过 equal()方法比较返回 true,并且它们的 hashCode()方法返回 true,HashSet 将不予添加。
LinkedHashSet,是 HashSet 的子类,在插入元素的时候,同时使用链表维持插入元素的顺序
SortedSet 接口有一个实现类:TreeSet(底层由平衡二叉树实现)确保集合中的元素都是出于排序状态
注意 LinkedHashSet 和 SortedSet 区别,前者存插入顺序,后者存插入之后的顺序
另外:
JDK5 : 桶表 + 链表
JDK8 : 桶表 + 链表 + 二叉树
- **二叉树**: 检索深度 > 8 的时候, 转化为二叉树, 减少查询深度
HashSet — HashMap 的源码实现
1 | /** |
TreeSet的默认排序
- TreeSet是有序的不可重复的, 有序是指元素值的大小
- 数值类型: 按照大小进行升序排序
- 字符串类型: 按照字典顺序进行升序排序
- 字符串从左到右, 一位一位的比较
- 自定义TreeSet类型: 实现
compareTo
方法, 返回为0的情况会默认覆盖
1.2.2、Map 系
Map 特点:存储的元素是键值对,在 JDK1.8 版本中是 Node,在老版本中是 Entry
Map 接 口 有 五 个 常用 实 现 类 : HashMap , HashTable , LinkeHashMap , TreeMap ,
ConcurrentHashMap
1. HashMap & Hashtable 的区别
HashMap
1. 非线程安全, 效率高
2. key不可以重复
3. key可以为null, 但只能有一个key为null
Hashtable
- 线程安全, 效率低
- key不可以重复
- 不可以为null
2. concurrentHash 简单分析
是从 JDK1.5 之后提供的一个 HashTable 的替代实现,采 一个 map 中的元素分成很多的 segment,通过 lock 机制可以对每个 segment 加读写锁,从 而提高 map 的效率,底层实现采用数组+链表+红黑树的存储结构
- Java并发包中的, 既是线程安全的, 又不至于效率过低
- 怎么实现: 分段锁机制
- 分段锁: 只加载在某一段数据上
- MySql: 查询 - 95%, 增删改 - 5%
- 读锁: 共享锁, 一个线程进行操作的时候不应吸纳另一个线程的结构
- 写锁: 排它锁, 一个线程在进行操作的时候不允许其他任何线程的操作
3. put & get 的流程
put 的大致流程如下:
- 通过 hashcode 方法计算出 key 的 hash 值
- 通过 hash%length 计算出存储在 table 中的 index(源码中是使用 hash&(length-1),这样结 果相同,但是更快)
- 如果此时 table[index]的值为空,那么就直接存储,如果不为空那么就链接到这个数所在 的链表的头部。(在 JDK1.8 中,如果链表长度大于 8 就转化成红黑树)
get 的大致流程如下:
通过 hashcode 计算出 key 的 hash 值
通过 hash%length 计算出存储在 table 中的 index(源码中是使用 hash&(length-1),这样结 果相同,但是更快)
- 遍历 table[index]所在的链表,只有当 key 与该节点中的 key 的值相同时才取出。
1.3 掌握重点
List: ArrayList, LinkList
Set: HashSet, TreeSet
- 需要掌握的方法: add , get, contains
Map: HashMap, TreeMap
- 需要掌握的方法: put get map的循环遍历 containsKey….
以上的都需要跟下源码
1.4 功能方法
1.4.1 List 的功能方法
ArrayList: 由数组实现的 List。允许对元素进行快速随机访问,但是向 List 中间插入与移 除元素的速度很慢。ListIterator 只应该用来由后向前遍历 ArrayList,而不是用来插入和移除 元素。因为那比 LinkedList 开销要大很多。
LinkedList : 对顺序访问进行了优化,向 List 中间插入与删除的开销并不大。随机访问则 相对较慢。(使用 ArrayList 代替。)还具有下列方 法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和 removeLast(), 这些方法 (没有在任何接口或基类中定义过)使得 LinkedList 可以当作堆栈、队列和双向队列使用。
1.4.2 Set的功能方法
Set : 存入 Set 的每个元素都必须是唯一的,因为 Set 不保存重复元素。加入 Set 的元素 必须定义 equals()方法以确保对象的唯一性。Set 与 Collection 有完全一样的接口。Set 接口 不保证维护元素的次序。
HashSet : 为快速查找设计的 Set。存入 HashSet 的对象必须定义 hashCode()。
TreeSet : 保存次序的 Set,底层为树结构。使用它可以从 Set 中提取有序的序列。
LinkedHashSet : 具有 HashSet 的查询速度,且内部使用链表维护元素的顺序(插入的次 序 。于是在使用迭代器遍历 Set 时,结果会按元素插入的次序显示。
1.4.3 Map 的功能方法
Map : 维护“键值对”的关联性,使你可以通过“键”查找“值”
HashMap : Map 基于散列表的实现。插入和查询“键值对”的开销是固定的。可以通过 构造器设置容量 capacity 和负载因子 load factor,以调整容器的性能。
LinkedHashMap : 类似于 HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插 入次序,或者是最近最少使用(LRU)的次序。只比 HashMap 慢一点。而在迭代访问时发而更 快,因为它使用链表维护内部次序。
TreeMap : 基于红黑树数据结构的实现。查看“键”或“键值对”时,它们会被排序(次 序由 Comparabel 或 Comparator 决定)。TreeMap 的特点在 于,你得到的结果是经过排序的。 TreeMap 是唯一的带有 subMap()方法的 Map,它可以返回一个子树。
WeakHashMap : 弱键(weak key)Map,Map 中使用的对象也被允许释放: 这是为解决特 殊问题设计的。如果没有 map 之外的引用指向某个“键”,则此“键”可以被垃圾收集器回 收。
IdentifyHashMap : 使用==代替 equals()对“键”作比较的 hash map。专为解决特殊问题 而设计。
2、反射
2.1 反射
反射: 将 Java 类中的各个成分 (属性, 方法, 构造方法) 映射成对应的类
- 在运行时判断任意一个对象的所属的类 Class。
- 在运行时判断构造任意一个类的对象 Constructor。
- 在运行时判断任意一个类所具有的成员变量 Field 和方法 Method。
- 在运行时调用任意一个对象的方法。method.invoke(object, args)
反射的好处
- 提高了整个代码的灵活性
- 不需要知道细节
反射用的最多的时候, 就是写框架的时候
反射中需要掌握3个类:
- Constructor: 构造器的描述类
- Field: 属性的描述类
- Method: 方法的描述类
Java 预定义类型
是否是预定义类型: isPromitive()
, 8种基本数据类型 + void 都是预定义类型
引用类型, 包装类不是预定义类型.
1 | System.out.println(int.class.isPrimitive()); // true |
2.2 Class
Class : 用于描述所有类的类, Class 类描述了类的属性信息,如类名、访问权限、包名、字 段名称列表、方法名称列表等, Class就是反射的基础.
获取Class的3种方式
1. `Class.forName`("类名字符串") (注意:类名字符串必须是全称,包名+类名)
- 如果 `.class`已经被加载到内存了, 直接返回
- 如果没有的话, 就先加载到内存
2. `类名.class`
3. `实例对象.getClass()`
2.3 Constructor
1 | // API |
2.4 Field
Field
类代表某个类中的一个成员变量,设有一个 obj 对象,Field 对象不是 obj 具体的变量值, 而是指代的是 obj 所属类的哪一个变量,可以通过 Field(对象).get(obj)
获取相应的变量值
1 | // API |
2.5 Method
Method 类代表某个类中的成员方法
Method 对象不是具体的方法,而是来代表类中哪一个方法,与对象无关
1 | // 获取: 得到类中某一个方法: |
3. 设计模式
设计模式(Design pattern)代表了面向对象编程中最佳的实践,通常被有经验的面向对象的 软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方 案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
设计模式只不过针对某些具体场景提供了一些效率较高的以复杂度换灵活性的手段而已
3.1 设计模式 – 六大原则
总原则:开闭原则(Open Close Principle)
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的 代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的 扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等,后面的 具体设计中我们会提到这点。
六大原则:
单一职责原则
不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就 应该把类拆分。
里氏替换原则(Liskov Substitution Principle)
里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。
依赖倒转原则(Dependence Inversion Principle)
这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码 时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
接口隔离原则(Interface Segregation Principle)
这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将 接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。
迪米特法则(最少知道原则)(Demeter Principle)
就是说:一个类对自己依赖的类知道的越少越好 。也就是说无论被依赖的类多么复杂,都 应该将逻辑封装在方法的内部,通过 public 方法提供给外部。这样当被依赖的类变化时,才 能最小的影响该类。
合成复用原则(Composite Reuse Principle)
原则是尽量首先使用合成/聚合的方式,而不是使用继承。
3.2 设计模式 – 分类
总体来说设计模式分为三大类:
- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合 模式、享元模式。
- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模 式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
3.3 常见设计模式
3.3.1 单例模式(手写)
单例模式(Singleton Pattern)是 Java 中最简单的,也是最最最常用的设计模式之一。这种类 型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
共有六种实现:
1、懒汉式,线程不安全
1 | public class Singleton { |
2、懒汉式,线程安全
1 | public class Singleton { |
3、饿汉式
1 | public class Singleton { |
4、双检锁/双重校验锁(DCL,即 double-checked locking) –面试必备
1 | public class Singleton { |
5、登记式/静态内部类
1 | public class Singleton { |
6、枚举
1 | public enum Singleton { |
3.3.2 装饰器模式(手写)
首先看一段代码
代码分析:
- 构造一个缓冲的字符输入流。包装了一个文件字符输入流。
- 事实上,BufferedReader 就是用来增强 FileReader 的读取的功能的。
- FileReader 只有 read()方法, 但是 BufferedReader 中却增加了一个 readLine()的逐行读取 的功能
- 所以这就相当于是 BufferedReader 装饰了 FileReader,让 FileReader 变得更强大
装饰器模式概念
- 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结 构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
- 这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下, 提供了额外的功能。
装饰器模式实现方式
1. 定义包装类
2. 将要装饰的模式作为参数传入包装类
3. 实现要加强的方法
3.3.2 代理模式
1. 静态代理
静态代理的缺点很明显:一个代理类只能对一个业务接口的实现类进行包装,如果有多个业 务接口的话就要定义很多实现类和代理类才行。
…
2. 动态代理
第一种:JDK 动态代理实现
JDK 动态代理所用到的代理类在程序调用到代理类对象时才由 JVM 真正创建,JVM 根据传 进来的业务实现类对象以及方法名,动态地创建了一个代理类的 class 文件并被字节码引擎 执行,然后通过该代理类对象进行方法调用。我们需要做的,只需指定代理类的预处理、 调用后操作即可。
只能对实现了接口的类生成代理,而不是针对类,该目标类型实现的接口都将被代理。原理 是通过在运行期间创建一个接口的实现类来完成对目标对象的代理。具体实现步骤:
- 定义一个实现接口
InvocationHandler
的类 - 通过构造函数或者静态工厂方法等,注入被代理类
- 实现
invoke(Object proxy, Method method, Object[] args)
方法 - 在主函数中获得被代理类的类加载器
- 使用
Proxy.newProxyInstance(classLoader, interfaces, args)
产生一个代理对象 - 通过代理对象调用各种方法
1 | =============//实现InvocationHandler==================== |
第二种:CGLIB 动态代理实现:
CGLIB 是针对类来实现代理的,原理是对指定的业务类生成一个子类,并覆盖其中业务方法 实现代理。因为采用的是继承,所以不能对 final 修饰的类进行代理,final 的方法也不能
针对类实现代理,对是否实现接口无要求。原理是对指定的类生成一个子类,覆盖其中的方 法,因为是继承,所以被代理的类或方法最好不要声明为 final 类型。具体实现步骤:
1、定义一个实现了 MethodInterceptor
接口的类
2、实现其 intercept()
方法,在其中调用 proxy.invokeSuper()
3. 静态代理和动态代理的区别
静态代理:自己编写创建代理类,然后再进行编译,在程序运行前,代理类的.class 文件就 已经存在了。
动态代理:在实现阶段不用关心代理谁,而在运行阶段(通过反射机制)才指定代理哪一个 对象。
3.4 重点掌握
3.4.1. 装饰者模式 和 静态代理模式 区别
在代码上的区别:
- 一般情况下, 装饰者模式被装饰的对象一般是从外部传入, 装饰的是一类的事务, 只要是某一类的(Class)都可以
- 静态代理模式被代理对象的初始化一般是内部创建的, 代理的是一个类的对象.
从功能上:
- 装饰者模式, 用于对被装饰者业务逻辑实现或增强, 对方法名没有要求
- 静态代理: 主要用于权限控制, 日志打印, 错误预警等功能
3.4.2. 三种设计模式必须掌握的
- 单例设计模式
- 装饰者模式
- 动态代理模式
3.4.3. 手写代码
- 冒泡排序
- 快速排序
- 设计模式
- hadoop 的 wordcount
- scala 的 wordcount
- spark 的 wordcount
4. 排序算法
核心概念:算法复杂度、稳定性
算法复杂度:算法复杂度是指算法在编写成可执行程序后,运行时所需要的资源,资源包括 时间资源和内存资源。应用于数学和计算机导论。
稳定性:一个排序算法是稳定的,就是当有两个相等记录的关键字 R 和 S,且在原本的列表 中 R 出现在 S 之前,在排序过的列表中 R 也将会是在 S 之前。
4.1. 排序分类
按照排序结果是否稳定性分类:
- 稳定排序:插入排序,冒泡排序,归并排序,计数排序,基数排序,桶排序(如果桶内 排序采用的是稳定性排序)
- 非稳定排序:选择排序,快速排序,堆排序。
按照排序过程中是否需要额外空间:
- 原地排序:插入排序,选择排序,冒泡排序,快速排序,堆排序。
- 非原地排序:归并排序,计数排序,基数排序,桶排序。
按照排序的主要操作分类:
- 交换类:冒泡排序、快速排序;此类的特点是通过不断的比较和交换进行排序;
- 插入类:简单插入排序、希尔排序;此类的特点是通过插入的手段进行排序;
- 选择类:简单选择排序、堆排序;此类的特点是看准了再移动;
- 归并类:归并排序;此类的特点是先分割后合并;
按照是否需要比较分类:
- 比较排序,时间复杂度 O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序, 归并排序,堆排序,快速排序等。
- 非比较排序,时间复杂度可以达到 O(n),主要有:计数排序,基数排序,桶排序等。
4.2 常见排序的时间复杂度
4.3 常见排序算法的核心实现
4.3.1 冒泡排序
4.3.2 归并排序
4.3.3 快速排序