Java基础知识点
2022-01-15 20:04:56 1 举报
AI智能生成
学习尚硅谷宋红康老师及狂神的教程,写的学习思维导图
作者其他创作
大纲/内容
集合类
集合概述
集合、数组都是对多个数据进行存储操作的结构,简称 Java容器
说明
此时的存储
主要指的是内存层面的存储
不涉及持久化的存储(.txt...)
数组
数组在多个数据方面的特点
一旦初始化以后,其长度就确定了
数组一旦定义好,其元素的类型也就确定了。我们只能操作指定类型的数据了
比如
String[] arr
int[] arr
Object[] arr
数组在存储多个数据方面的缺点
一旦初始化以后,其长度就不可修改
数组中提供的方法非常优先,对于添加、删除、插入数据等操作,非常不方便,同时效率不高
获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
数组存储数据的特点
有序、可重复
对于无序、不可重复的需求,不能满足
集合存储的优点
解决数组存储数据方面的弊端
Java集合可分为Collection和Map两种体系
Collection接口
Map接口
Java集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组
Collection接口
单列数据,定义了存取一组对象的方法的集合
Collection接口继承树
方法
add(Object e)
将元素e添加到集合coll中
addAll(Collection c)
将另一集合元素全部添加到集合coll中
size()
获取添加的元素个数
clear()
清空集合元素
isEmpty()
判断当前集合是否为空
contains(Object obj)
判断当前集合中是否包含obj
containAll(Collection coll)
判断形参coll中的所有元素是否都存在于当前集合中
retainAll(Collection coll)
交集
获取当前集合和coll集合的相同元素
equals(Object obj)
判断当前集合和形参集合是否相同
若是List
判断元素、顺序
若是Set
判断元素
remove(obj)
移出obj元素
如何判断是否存在:内部调用equals()
若不存在,报异常
removeAll(collection coll)
从当前集合中移除coll中所有的元素
如何判断是否存在:内部调用equals()
举例
a删b,若b存在多于a的元素,也不会报错
hashCode()
返回当前对象的哈希值
toArray()
集合 --> 数组
举例
拓展:数组 --> 集合
Array.asList(T ... t)
注意点
转换基本数据类型数组时,需要注意,否则可能把整个int[]类型当作一个 元素
实例
iterator()
概念
Collection接口继承了java.lang.Iterable接口,该接口有iterator()方法,所以该接口实现了iterator()
集合对象 每次调用iterator()方法 都会 得到 一个 全新的迭代器对象,默认的游标在 集合的 第一个元素之前
返回Iterator接口的实例,用于遍历集合元素
注意
向Collection接口的实现类的对象中添加数据obj时,要求obj所在的类要重写equals()
方便后续调用contains()等方法作比较
List接口
List接口简介
存储 有序的、可重复 的数据
“动态”数组
鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组
JDK1.2新增
ArrayList集合
概念
作为List接口的主要实现类
线程不安全,效率高
底层使用 Object[] elementData 存储,顺序表 存储结构
JDK1.2新增
源码(超长警告)
JDK 7情况下
调用构造语句
来到构造器这边,调用 重载的构造器,传入参数 10(默认长度)
最后一句
this.elementData = new Object[ 10 ];
让当千存储数据的数组,为长度10的数组
this.elementData = new Object[ 10 ];
让当千存储数据的数组,为长度10的数组
存储数据的数组
当调用add添加元素时
进入add方法
这里的size,就是实际包含的元素个数
ensureCapacityInternal()
在添加之前肯定要确保当前的容量足够,先调用一下保证没问题
在添加之前肯定要确保当前的容量足够,先调用一下保证没问题
modCount++ 先不讲,是一个快速失败机制的
来到这边,比如当前size为1,默认长度为10,那么相减小于0
那就不会执行到扩容方法,退出到上一层
那就不会执行到扩容方法,退出到上一层
若来到这边,比如当前size为10,默认长度为10,那么相减<0
开始执行扩容方法
开始执行扩容方法
当前 minCapacity 为 11
1. 让oldCapacity(oC) = 当前的存储数据的实际长度
2. 让newCapacity(nC) = oC + oC/2 = 1.5 * oC
3. 如果 nC 小于 11
那就让 nC = 11
4. 如果 nC 大于我们提前设置 的 数组最大值,进入该方法
1. 如果 传过来的 mC < 0
数据溢出
那就抛出异常了
2. 判断 mC 是否大于 我们提前设置的数组MAX值
大于
让他的值为 Integer最大值
小于
其实按照逻辑,是 等于
等于的话,就直接设置为 提前设置的数组MAX值
5. 让 存储数据的数组elementData = 新创 长度为nC 的 数组(把旧数据搬过来)
第二行是赋值操作,且没问题返回true
老师的概括
建议
开发中使用带参构造器 : ArrayList list = new ArrayList(int capacity)
JDK 8情况下
调用构造语句
来到构造器这边,给了一个 DEE 给 elementData
DEE是什么?
相当于懒汉式提前创建好的 {} ,啥没有,等你添加再来创建
相当于懒汉式提前创建好的 {} ,啥没有,等你添加再来创建
存储数据的数组
当调用add添加元素时
进入add方法
这里的size,就是实际包含的元素个数
ensureCapacityInternal()
在添加之前肯定要确保当前的容量足够,先调用一下保证没问题
在添加之前肯定要确保当前的容量足够,先调用一下保证没问题
来到这边,比如当前size为1,默认长度为10,那么相减小于0
那就不会执行到扩容方法,退出到上一层
那就不会执行到扩容方法,退出到上一层
判断一下 你目前 的elementData 是不是 DEE?
也就是还没添加任何东西,刚创建
那么让 minCapacity(mC) = 默认设置数(10) 和 (实际元素个数+1)的最大值
然后进入 ensureExplicitCapacity()
modCount++ 先不讲,是一个快速失败机制的
来到这边,比如当前 mC为10,长度已经16了,那么相减小于0
那就不会执行到扩容方法,退出到上一层
那就不会执行到扩容方法,退出到上一层
来到这边,比如当前 mC为10,长度为10了,那么相减=0
开始执行扩容方法
开始执行扩容方法
然后这边和JDK7 完全一致
1. 让oldCapacity(oC) = 当前的存储数据的实际长度
2. 让newCapacity(nC) = oC + oC/2 = 1.5 * oC
3. 如果 nC 小于 11
那就让 nC = 11
4. 如果 nC 大于我们提前设置 的 数组最大值,进入该方法
1. 如果 传过来的 mC < 0
数据溢出
那就抛出异常了
2. 判断 mC 是否大于 我们提前设置的数组MAX值
大于
让他的值为 Integer最大值
小于
其实按照逻辑,是 等于
等于的话,就直接设置为 提前设置的数组MAX值
5. 让 存储数据的数组elementData = 新创 长度为nC 的 数组(把旧数据搬过来)
第二行是赋值操作,且没问题返回true
老师的概况
Arraylist list = new ArrauList(); // 地城Object[] elementData 初始化为{},并没有创建长度为10的数组
list.add(123); //第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData中
...
后续的添加和扩容操作与 jdk7 无太大差别,基本一致
list.add(123); //第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData中
...
后续的添加和扩容操作与 jdk7 无太大差别,基本一致
LinkedList集合
概念
底层使用 双向链表 结构存储
对于频繁的插入、删除操作,使用此类效率比ArrayList高
JDK1.2新增
源码(超长警告)
Vector集合
概念
作为List接口的古老实现类
线程安全,效率低
底层使用 Object[] elementData 存储,顺序表 存储结构
JDK1.0诞生,甚至比List接口诞生还早
JDK1.2 List接口诞生后,开始归为List接口范围
源码(超长警告)
调用构造语句
和ArrayList JDK7 无区别
当调用add添加元素时
这里就有区别了,这块是 synchronized,线程安全的
modCount++ 先不讲,是一个快速失败机制的
elementCount(eC) = 当前元素长度
进入 ensureCapacityHelper
添加之前肯定要确保当前的容量足够,先调用一下保证没问题
添加之前肯定要确保当前的容量足够,先调用一下保证没问题
来到这边,比如当前eC为1,默认长度为10,那么相减小于0
那就不会执行到扩容方法,退出到上一层
那就不会执行到扩容方法,退出到上一层
若来到这边,比如当前eC为10,默认长度为10,那么相减<0
开始执行扩容方法
开始执行扩容方法
当前 minCapacity 为 11
这块和JDK7 的 ArrayList 也稍微有所区别,主要是倍数不同。大体一致
1. 让oldCapacity(oC) = 当前的存储数据的实际长度
capacityIncrement(cI)默认为0
2. 让newCapacity(nC) = oC + (cI大于0输出cI,否则oC) = 2*oC
3. 如果 nC 小于 11
那就让 nC = 11
4. 如果 nC 大于我们提前设置 的 数组最大值,进入该方法
这块是JDK7 的 ArrayList源码,并不是Vector的
只是差不多,作个参考,意思差不多
这块是JDK7 的 ArrayList源码,并不是Vector的
只是差不多,作个参考,意思差不多
1. 如果 传过来的 mC < 0
数据溢出
那就抛出异常了
2. 判断 mC 是否大于 我们提前设置的数组MAX值
大于
让他的值为 Integer最大值
小于
其实按照逻辑,是 等于
等于的话,就直接设置为 提前设置的数组MAX值
5. 让 存储数据的数组elementData = 新创 长度为nC 的 数组(把旧数据搬过来)
第三行是赋值操作,且没问题返回true
老师的概况
JDK7 和 JDK8 中通过 Vector() 构造器创建对象时,底层都创建了 长度为10的 数组
在扩容方面,默认扩容为原来数组长度的两倍
在扩容方面,默认扩容为原来数组长度的两倍
现在就算需要线程安全也不用他, 用 ↑ 这个
子类
Stack
栈
方法
void add(int index, Object ele)
在index位置 插入ele元素
注意
别和addAll搞混了
add,把形参元素作为整体添加
addAll,把形参元素分别添加
常用方法
增
void add(Object obj)
在最后位置插入元素
删
Object remove(int index)
移除指定index位置的元素,并返回此元素
remove(Object obj)
删除指定元素
改
Object set(int index, Object ele)
设置指定index位置的元素为ele
查
get(int index)
查询index位置的元素
插
boolean addAll(int index , Collection eles)
从index位置开始将eles中的所有元素添加进来
长度
size()
查询当前长度
遍历
Iterator迭代器方式
增强for循环
普通的循环
实例
Object get(int index)
获取指定index位置的元素
int indexOf(Object obj)
返回obj在集合中首次出现的位置
如果不存在,返回-1
int lastIndexOf(Object obj)
返回obj在当前集合中末次出现的位置
如果不存在,返回-1
List subList(int fromIndex, int toIndex)
返回从fromIndex到toIndex位置的子集合
左闭右开
面试题
区分 List中 remove(int index) 和 remove(Object obj)
正确答案:编译后 [1,2]
若是想得到 [1,3]
请这样做
若是 关于 方法参数的值传递机制 有疑惑?自行搜索本文档,有详解
Set接口
Set接口简介
存储无序的、不可重复的数据
高中讲的“集合”
特性
无序性
不等于随机性
存储的数据在底层数组中并非按照数组索引的顺序添加的,而是根据数据的哈希值决定的
不可重复性
Set判断两个对象是否相同 => hashCode(),equals() 比较都要相等 ,而不是 == 运算符
相同元素只能添加一个
相等的对象必须具有相等的散列码
set接口没有提供额外的方法
HashSet集合
概念
作为Set接口的主要实现类
特点
线程不安全的,效率高
可以存储null值
判断两个元素相等的标准
通过 hashCode(),equals() 比较都要相等
对于存放在 Set 容器的对象
对应的类一定要重写 equals()和hashCode()方法,以实现对象相等规则
底层结构
JDK1.2 - JDK7
数组+链表的结构
JDK8
HashMap
LinkedHashSet集合
概念
作为HashSet的子类
遍历其内部数据时,可以按照添加的顺序遍历
底层结构
在HashSet原有的基础之上,每个维护了两个引用(pre、next)以此来记录前一个数据和后一个数据
类似双向链表,来记录我们的添加顺序
类似双向链表,来记录我们的添加顺序
对于频繁的遍历操作,LinkedHashSet效率要高于HashSet
源码(超长警告)
内部存放了一个属性,记录当前存储数据 HashMap类 的 map
调用构造语句时
看,底层其实给我们 创建了一个 HashMap
底层其实给我们创建了一个HashMap,想看怎么构造HashMap,去HashMap那边看
存储数据时
调用 add()方法
看到没有,其实还是调用 HashMap 那边的put方法
就是相当于把 HashSet 一个数据 存到 HashMap 的 key 位置,
存储的 Value 就是默认的 PRESENT 常量
存储的 Value 就是默认的 PRESENT 常量
PRESENT
这里是静态,也就是说,大家的 key 其实都共用 一个value —— PRESENT
1. 调用 HashMap 的 put 方法,存储数据
具体实现看我 HashMap 那边
老师的总结
TreeSet集合
概念
可以按照添加对象的指定属性,进行排序
底层结构:红黑树
可进行自动排序
前提:向TreeSet中添加的数据,要求是相同类的对象
两种排序方式
自然排序(实现Comparable接口)
比较两个对象是否相同的标准
compareTo()返回0
而不是equals()
定制排序(Comparator)
比较两个对象是否相同的标准
compare()返回0
而不是equals()
使用方式
声明Comparator对象,并重写compare()方法
最后在声明 TreeSet 时,把Comparator对象传参定义就好。
Tree set = new TreeSet(com);
Tree set = new TreeSet(com);
实例
重写hashCode()和equals()
要向Set中添加数据,其所在的类一定要重写hashCode()和equals()
重写hashCode()方法的基本原则
多次调用hashCode()方法应该返回相同的值
equals()为true时,hashCode()也应该为true
用作equals()方法比较的Field,也应该用来计算hashCode值
问题:31数字怎么来的?
重写equals()方法的基本原则
相等的对象必须具有相等的散列码
equals()为true时,hashCode()也应该为true
用作equals()方法比较的Field,也应该用来计算hashCode值
重写小技巧
用作equals()方法比较的Field,也应该用来计算hashCode值
Iterator迭代器接口
概念
Itrator对象称为迭代器(设计模式的一种),主要用于遍历Collection集合的元素
迭代器模式
提供一种方法访问一个容器(contatiner)对象中各个元素,而又不需暴露该对象的内部细节
为容器而生
拓展:迭代器古老版本
Enumeration接口
java.util.Enumeration
JDK1.0
概念
Enumeration接口时Iterator迭代器的“古老版本”
使用范例
方法
next()
到下一个元素
指针下移
将下移以后集合位置上的元素返回
若下个记录无效,直接调用next()会抛出异常:NoSuchElementException
最好调用之前用 hasNext()进行检车
hasNext()
判断下一位还有元素否
remove()
删除指向的元素
注意点
不同于集合直接调用remove()
在指向为空时调用该方法会报异常:IllegalStateException
还没开始遍历,你要删
还未调用next()
当前指向删过一次了,还要删
在remove()过后,还不调用next()
实例
如何遍历
不同的遍历方式
若下个不存在,还调用next():报异常 - NoSuchElementException
foreach循环
概念
Java5.0 提供了 foreach循环 迭代访问 Collection和数组
遍历操作不需获取Collection或数组的长度,无需使用索引访问元素
遍历集合的 底层调用 Iterator 完成操作
使用方式
面试题
方式一会修改数组
方式二遍历是不会导致原数组被修改的
方式二遍历是不会导致原数组被修改的
Map接口
双列数据,保存具有映射关系“key-value对”的集合
类似于高中的函数:y = f(x)
概念
JDK1.2 出现
存储结构
Entry 对象
一个键值对:key-value 构成一个 Entry 对象
使用 Set存储所有的 Entry
无序的、不可重复的
Key
无序的、不可重复的
使用 Set 存储所有的 key
HashMap 的 key
使用 HashSet 存储所有的 key
导致:key 所在的类要重写 equals() 和 hashCode()
LinkedHashMap 的 key
使用 LinkedHashMap 存储所有的 key
导致:key 所在的类要重写 equals() 和 hashCode()
TreeMap 的 key
使用 TreeMap 存储所有的 key
导致:key 所在的类要重写 compareTo() / compare()
以此类推
Value
无序的、可重复的
使用 Collection 存储所有的 value
导致:value 所在的类要重写 equals()
Map接口继承树
方法
增
Object put(Object key, Object value)
将指定key-value添加到(或修改)当前map对象中
void putAll(Map m)
将m中的所有key-value对存放到当前map中
删
Object remove(Object key)
移除指定key的key-value对,并返回value
若找不到移除的数据,返回null
改
Object put(Object key, Object value)
将指定key-value添加到(或修改)当前map对象中
查
Object get(Object key)
获得指定key对应的value
长度
int size()
返回map中key-value对的个数
遍历
Set keySet()
返回所有key构成的Set集合
实例
遍历所有 key
遍历所有key-value
Collection values()
返回所有value构成的Collection集合
实例
遍历所有value
Set entrySet()
返回所有key-value对构成的Set集合
实例
遍历所有key-value
void clear()
清空当前map中的所有数据
与 map = null 操作不同
设置size = 0,然后挨个遍历底层数组各位置 = null
boolean containsKey(Object key)
是否包含指定的key
boolean containsValue(Object value)
是否包含指定的value
找value,找到一个返回true,就不继续往后找了
boolean isEmpty()
判断当前map是否为空
boolean equals(Object obj)
判断当前map和参数对象obj是否相等
HashMap
概念
作为Map的主要实现类,JDK1.2
线程不安全的,效率高
可 存储null的key和value
底层
JDK7之前
数组+链表
JDK8
数组+链表+红黑树
LinkedHashMap
概念
保证在遍历map元素时,可以按照添加的顺序实现遍历
原因
在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素
对于频繁的遍历操作,此类 执行效率高于 HashMap
JDK1.4 诞生
注重点
LinkedHashMap 与 HashMap 底层主要区别
Entry
LinkedHashMap.Entry 类继承了 HashMap.Node
但是自己多声明了两个属性,before,after
但是自己多声明了两个属性,before,after
多出这两个属性,就可以知道前一个和后一个元素,
这样就能按顺序遍历了
这样就能按顺序遍历了
HashMap.Node
就是一个 单向链表 的构造
想看全程看源码解析
源码(超长警告)
LinkedHashMap 继承 HashMap 实现 Map接口
调用构造语句时
调用父类构造方法
存储数据时
他还是调用的 HashMap 的put(),LinkedHashMap 并没有 重写
1. 调用hash()得到key的hash值
hash()
后面这一堆操作就是为了 降低冲突概率罢了(若以后有机会详细刨析一下)
这边和HashMap JDK8 那边完全一致,那么不同在哪里呢?
就是第3步,里面有个newNode,这个方法被重写了
就是第3步,里面有个newNode,这个方法被重写了
调用 putValue():噩梦来啦哈哈哈哈哈
1. 先创建
空的Node数组 tab,
空的Node p,
声明遍历n、i
空的Node数组 tab,
空的Node p,
声明遍历n、i
2. 判断 (当前hashmap的底层数组是否为空?)
tab = 当前hashMap数组,是否为空?
n = tab(当前数组)的长度,是否为0?
tab = 当前hashMap数组,是否为空?
n = tab(当前数组)的长度,是否为0?
首次调用,是空
1. 那就调用 resize():
调用完后,返回一个我们创建好的数组
调用完后,返回一个我们创建好的数组
在 resize() 中,帮我们创建好底层数组
resize()
1. Node数组oldTab = 当前数组(目前还是null)
2. 当前oT(旧数组)是null?
是 => oldCap(旧容量) = 0
否 => oldCap(旧容量) = 当前oT容量
是 => oldCap(旧容量) = 0
否 => oldCap(旧容量) = 当前oT容量
3. oldThr(旧临界因子) = 临界因子(目前无值,是0)
4. newCap(新容量) ,newThr(新临界因子) = 0
5.如果oC > 0 ?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1.旧容量(0) >= MAXIMUM_CAPACITY(hashmap最大支持容量)?
MAXIMUM_CAPACITY
HashMap的最大支持容量
是
1. 加载因子 = Integer最大数值
2. 返回 旧数组
都大于最大值了,肯定存不下了,给你返回去
否
判断
(新容量 = 旧容量*2 且 < hashmap最大支持容量吗?)&&
(旧容量 >= 默认初始化容量吗?)
(新容量 = 旧容量*2 且 < hashmap最大支持容量吗?)&&
(旧容量 >= 默认初始化容量吗?)
DEFAULT_INITIAL_CAPACITY
默认初始化值
是
新临界因子 = 旧临界因子 * 2
否
出去
这一步过后,新容量*2,临界因子也*2
6. 老临界因子 > 0?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. 新容量 = 老临界因子
这步我没看懂,以后有机会来,因为目前不会走到这里面
7. 上面两个绿色判断都不满足?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. 新容量 = 默认初始容量 = 16
DEFAULT_INITIAL_CAPACITY
默认初始化值
2. 新临界因子 = (默认加载因子 * 默认初始容量)的整数
12 0.75 * 16
12 0.75 * 16
DEFAULT_INITIAL_CAPACITY
默认初始化值
DEFAULT_LOAD_FACTOR
HashMap的默认加载因子
8. 新临界因子是0吗?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. ft = 新容量 * 加载因子
2. 新临界因子 = 一堆操作后的某个值
因为没进来,我就不想解了
9. 临界因子 = 新临界因子(12)
10. 然后我们创建 容量为新容量(16)的Node数组 = newTab
11. 判断我们的 旧数组是不是空?
是 => 下一步
否 => 进来
是 => 下一步
否 => 进来
这里就是一系列把 旧数组数据 搬到 新数组中的操作
以后无聊可以来详解一下
以后无聊可以来详解一下
12. 返回我们创建好的 newTab
2. tab = 刚创建好的数组
n = 创建好的数组长度
n = 创建好的数组长度
3. (判断要存储位置是否无值,无值直接存)
tab目前位置所对应的Node 赋给了 p,
并且判断 p 是否为空?
是 => 进来
否 =>下一步
tab目前位置所对应的Node 赋给了 p,
并且判断 p 是否为空?
是 => 进来
否 =>下一步
创建新Node(存入要存的key-value)到底层数组tab中 的 i 位置
添加成功!!!!!!!!!!!!!!!!!!!!!-- 情况1
添加成功!!!!!!!!!!!!!!!!!!!!!-- 情况1
重点就是这里,主要的区别和 HashMap JDK8 不同
LinkedHashMap重写了newNode方法
1. 调用newNode(),返回创建的p
(这是一个链表,且具有before和after属性,可以记录前后数据,带顺序)
(这是一个链表,且具有before和after属性,可以记录前后数据,带顺序)
Entry
LinkedHashMap.Entry 类继承了 HashMap.Node
但是自己多声明了两个属性,before,after
但是自己多声明了两个属性,before,after
多出这两个属性,就可以知道前一个和后一个元素,
这样就能按顺序遍历了
这样就能按顺序遍历了
HashMap.Node
就是一个 单向链表 的构造
4. 上面判断为否?
是 => 进来
是 => 进来
说明你要存储的位置 有值
1. 创建Node e,k
2. 判断 (你当前要存的key是否与当前位置所存在的key相同?)
当前位置值的哈希值是否与 你要存的 哈希是否相同? &&
k(当前位置的key) 是否 与你传入的 key 相同? ||
(key 不为空 && 调用equal()判断k是否与key相等?)
是 => 进来
否 => 下一步
当前位置值的哈希值是否与 你要存的 哈希是否相同? &&
k(当前位置的key) 是否 与你传入的 key 相同? ||
(key 不为空 && 调用equal()判断k是否与key相等?)
是 => 进来
否 => 下一步
你要是一样,我就取出当前位置的Node给e,
然后到 👉第5步,进行替换
然后到 👉第5步,进行替换
1. e = 当前位置底层数组对应的 Node
3. 判断 当前p是否是TreeNode类对象?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
这里就是说你是不是红黑树,是我就调用这种方法
1. 调用putTreeVal(),把当前p,数组,hash,key-value都存进去,然后把返回值赋给e
4. 上面两步都不成立?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
说明当前要存储的哈希值 与 当前存储位置的哈希值不相等,且不是红黑树
注意:是与第一个的哈希值不相等哟。因为存放一个位置可能链了好多个,所以开始了for循环
1. for循环遍历
遍历 初始值 binCount = 0
开始遍历
1. 判断当前位置p的next有没有值?
每次把 p的next 赋给 e(!!)
无 => 进来
有 => 下一步
每次把 p的next 赋给 e(!!)
无 => 进来
有 => 下一步
也就是说目前他存储的位置的next没有值了,所以我要存这里
1. 创建新Node(存入我们想要存放的key-value...)放到p.next
添加成功 !!!!!!!!!!!!!!!!! -- 情况 2
添加成功 !!!!!!!!!!!!!!!!! -- 情况 2
这就是 七上八下 的 八下
下面这步就是 如果 我们在Node[某位置]链了达到了8条,那么我们就要把当前位置的链式结构换成红黑树结构
2. 上面添加完成以后,我们还要判断下当前的 binCount 是否大于等于 红黑树临界容量(8)?
大于等于 => 有事,我们把这条链改成红黑树结构了,进来调用treeifyBin()方法,传入底层数组tab,和目前要存储的hash
小于 => 没事,继续下一步
大于等于 => 有事,我们把这条链改成红黑树结构了,进来调用treeifyBin()方法,传入底层数组tab,和目前要存储的hash
小于 => 没事,继续下一步
TREEIFY_THRESHOLD
Bucket中链表长度大于该默认值,转化为红黑树
treeifyBin()
1. 初始化 n,index,Node e
2. 判断 (当前数组长度是否大于被树化的最小容量?)
当前底层数组tab为空不?||
n = tab的长度 < Node被树化时最小的hash表容量吗?
小于 => 进来
大于 => 下一步
当前底层数组tab为空不?||
n = tab的长度 < Node被树化时最小的hash表容量吗?
小于 => 进来
大于 => 下一步
MIN_TREEIFY_CAPACITY
Bucket中的Node被树化时最小的hash表容量
当Bucket中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,
此时应执行resize操作扩容这个MIN_TREEIFY-CAPACITY的值至少是TREEIFY_THRESHOLD的4倍
此时应执行resize操作扩容这个MIN_TREEIFY-CAPACITY的值至少是TREEIFY_THRESHOLD的4倍
什么意思?
其实就是 我们单个位置链数以达到8了,我们想变成红黑树结构之前,还需要判断表容量
表容量默认16,我们至少要让表容量在64才可以变成红黑树结构,不然还是只能先扩容
表容量默认16,我们至少要让表容量在64才可以变成红黑树结构,不然还是只能先扩容
我们单个位置链数以达到8了,
我们想变成红黑树结构之前,还需要判断表容量表容量默认16,
我们至少要让表容量在64才可以变成红黑树结构,不然还是只能先扩容
我们想变成红黑树结构之前,还需要判断表容量表容量默认16,
我们至少要让表容量在64才可以变成红黑树结构,不然还是只能先扩容
1. 那就调用 resize():
调用完后,返回一个我们扩容好的数组
调用完后,返回一个我们扩容好的数组
这时候进入 resize() 中,帮我们扩容
resize()
1. Node数组oldTab = 当前数组(目前有数了,容量16)
2. 当前oT(旧数组)是null?
是 => oldCap(旧容量) = 0
否 => oldCap(旧容量) = 当前oT容量 = 16
是 => oldCap(旧容量) = 0
否 => oldCap(旧容量) = 当前oT容量 = 16
3. oldThr(旧临界因子) = 临界因子(目前无值,是0)
4. newCap(新容量) ,newThr(新临界因子) = 0
5.如果oC > 0 ?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1.旧容量(0) >= MAXIMUM_CAPACITY(hashmap最大支持容量)?
MAXIMUM_CAPACITY
HashMap的最大支持容量
是
1. 加载因子 = Integer最大数值
2. 返回 旧数组
都大于最大值了,肯定存不下了,给你返回去
否
判断
(新容量 = 旧容量*2 且 < hashmap最大支持容量吗?)&&
(旧容量 >= 默认初始化容量吗?)
(新容量 = 旧容量*2 且 < hashmap最大支持容量吗?)&&
(旧容量 >= 默认初始化容量吗?)
DEFAULT_INITIAL_CAPACITY
默认初始化值
是
新临界因子 = 旧临界因子 * 2
否
出去
这一步过后,新容量*2,临界因子也*2
6. 老临界因子 > 0?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. 新容量 = 老临界因子
7. 上面两个绿色判断都不满足?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. 新容量 = 默认初始容量 = 16
DEFAULT_INITIAL_CAPACITY
默认初始化值
2. 新临界因子 = (默认加载因子 * 默认初始容量)的整数
12 0.75 * 16
12 0.75 * 16
DEFAULT_INITIAL_CAPACITY
默认初始化值
DEFAULT_LOAD_FACTOR
HashMap的默认加载因子
8. 新临界因子是0吗?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. ft = 新容量 * 加载因子
2. 新临界因子 = 一堆操作后的某个值
因为没进来,我就不想解了
9. 临界因子 = 新临界因子
10. 然后我们创建 容量为新容量(32)的Node数组 = newTab
11. 判断我们的 旧数组是不是空?
是 => 下一步
否 => 进来
是 => 下一步
否 => 进来
这里就是一系列把 旧数组数据 搬到 新数组中的操作
以后无聊可以来详解一下
以后无聊可以来详解一下
12. 返回我们创建好的 newTab
3. 判断 (hash对应的tab存储位置 为空吗?)
e = tab对应要存储位置的Node,为空吗?
不为空 => 进来
为空 => 下一步
e = tab对应要存储位置的Node,为空吗?
不为空 => 进来
为空 => 下一步
1. 创建 TreeNode hd 为null,tl也为null
2. do while循环
开始遍历
这里就是 把原数组链的方式 转成 红黑树的方式
没讲了,老师没讲到这里,以后有机会详解把,目前了解到这里就差不多了
(有下一个不,有就继续遍历)
遍历条件:
e = e.next,
当e的next 是空时,结束循环
遍历条件:
e = e.next,
当e的next 是空时,结束循环
2. 判断 (你当前要存的key是否与当前位置的next所存在的key相同?)
当前位置的next的值的哈希值是否与 你要存的 哈希是否相同? &&
k(当前位置的next的key) 是否 与你传入的 key 相同? ||
(key 不为空 && 调用equal()判断k是否与p.next.key相等?)
是 => 进来
否 => 下一步
当前位置的next的值的哈希值是否与 你要存的 哈希是否相同? &&
k(当前位置的next的key) 是否 与你传入的 key 相同? ||
(key 不为空 && 调用equal()判断k是否与p.next.key相等?)
是 => 进来
否 => 下一步
1. break,退出循环
这时候说明 在当前位置不断next时,找到了一个 哈希值与我们存储哈希值相同的
然后到 👉第5步,进行替换
然后到 👉第5步,进行替换
3. p = e
进到这步说明 还有next,且next值不同
那么让 p = e,让p=p.next的意思
继续执行循环
那么让 p = e,让p=p.next的意思
继续执行循环
每次 ++
5. e 不为空吗?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
若为 要存储的key 和 存储位置的key 相同,就会走到这步
把 旧的value 替换成了 新的value
把 旧的value 替换成了 新的value
1. 把老value存到 oldVlaue中
2. 判断
非onlyIfAbsent(传过来是false) ||
老数据为空吗?
是 => 进来
否 => 下一步
非onlyIfAbsent(传过来是false) ||
老数据为空吗?
是 => 进来
否 => 下一步
1. 把 value 存入了e.value 中
替换成功!!!!!!!
替换成功!!!!!!!
3. 不知干啥
4. 返回老value
6. modCount ++
7. 判断 ++size 后 > 扩容因子不?
是 => 调用resize()方法扩容
否 => 下一步
是 => 调用resize()方法扩容
否 => 下一步
8. 调用 这个啥方法我也不清楚,以后解释
9. 返回 空
源码(超长警告)
JDK7
调用构造语句时
DEFAULT_INITIAL_CAPACITY
HashMap的默认容量
默认容量为16
DEFAULT_LOAD_FACTOR
HashMap的默认加载因子
默认的加载因子 = 0.75f
来到HashMap(16, 0.75)
1. iC是否小于 0
小于0,抛异常
2. iC是否大于MAXIMUM_CAPACITY
MAXIMUM_CAPACITY
HashMapd的最大支持容量
3. 判断lF(加载因子)是否小于0,且判断当前加载因子是否为空
这里默认传了0.75过来,不小于
若小于,则抛出异常
4. capacity(cp) = 1
5. 当cp小于iC时,cp左移一位,cp=cp*2
这里默认iC为16,也就是无论咋地,cp都会为16
最小16
最小16
6. 让当前加载因子 赋给 属性
7. 让threshold(扩容临界值)= (16 * 0.75和HashMap中最大支持容量+1)中取最小值
threshold
扩容的临界值
容量 * 填充因子 =
这里得到 threshold = 12
也就是表示他存储数据超过12时,就会开始扩容了
不是存满才扩的!这点需要注意。
因为一个底层数组,但存储位置我们是根据哈希值算的,所以可能会导致以下问题:
1. 同一个位置存储过多数据
2. 有几个位置一直空着,没存东西
1. 同一个位置存储过多数据
2. 有几个位置一直空着,没存东西
所以为了避免这样,我们会选择在超过临界值时,扩容,让数据分布更为平均。
8. 创建长度为16的Entry数组,赋给table
table
存储元素的数组,总是2的n次幂
HashMap底层存储数据的数组
Entry数组
9. 这块就暂时省略,后面我有机会详细说一下
存储数据时
1. 判断当前key是否为null,若是,那也放进去
这里就是 HashMap 和 Hashtable 的区别了
2. 让当前hash = key的hash值经过一系列算数得到一个值
注意:这个hash值并不是我们在数组中存放的位置
注意:这个hash值并不是我们在数组中存放的位置
useAltHashing默认为false
1. h = 0
2. uAH为false,不执行
3. h = 当前key的hashCode 异或 h
4. 后面这一堆操作就是为了 降低冲突概率罢了(若以后有机会详细刨析一下)
3. 经过indexFor(),得到存放位置i
通过 哈希值 与 16-1 来与运算,结果小于16
妙啊,这方法比 % 效率要高
4. 遍历for,进来细说这部分
遍历条件
拿出table[i]位置的Entry e
判断是否为空
不为空就执行
1. 目前存储的哈希值 是否与 e中的哈希值相同
若不等
那就下层循环
若相等
2. 判断key的地址是否相同?
其实主要目的是比较equals就行
但若key地址都相同,那就直接省事了,调用equals会一点一点比,效率会低很多
3. equals()判断 key和k是否相同?
true
oldValue(oV) = 当前e的value
把 e.value 换成 新的value
相当于覆盖操作
记录当前对象
返回 oV
false
那就下层循环
循环条件:调用 Entry.next,拿下一个
5. 若上面判断为false往下走
6. modCount++
7. 调用addEntry 添加key-value
addEntry()
1. 判断
目前大小 是否大于 扩容临界
当前table[i位置] 是否 有值
目前大小 是否大于 扩容临界
当前table[i位置] 是否 有值
这里的i是想要存储的位置噢
上面判断为ture
开始扩容
1. 扩容为 原来的长度*2
2. 若key为空,hash = 0,否则hash = key的哈希值
3. BuckIndex = 调用indexFor()重新算一遍当前应该存放的位置
通过 哈希值 与 16-1 来与运算,结果小于16
妙啊,这方法比 % 效率要高
上面判断为false
调用createEntry()
1. 先把原有位置的 Entry 先取出
调用 Entry() ,把新Entry 存入 底层数组中
Entry()
让 当前的 next 指向 原有的Entry
别的就是正常赋值
size++
8. 返回空
老师总结
HashMap map = new HashMap();
在实例化以后,底层创建了长度是16的一维数组 Entry[] table。
在实例化以后,底层创建了长度是16的一维数组 Entry[] table。
map.put(key1, value1);
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算后,得到在Entry数组中的存放位置。-①
如果此位置上的数据为空,此时的key1-value1添加成功。 --情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。 --情况2
如果key1的哈希值和已经存在的某个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法:
如果equals()返回false:此时key1-value1添加成功。 --情况3
如果equals()返回true:使用value1替换value2。
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算后,得到在Entry数组中的存放位置。-①
如果此位置上的数据为空,此时的key1-value1添加成功。 --情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。 --情况2
如果key1的哈希值和已经存在的某个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法:
如果equals()返回false:此时key1-value1添加成功。 --情况3
如果equals()返回true:使用value1替换value2。
①处说明
这里的到的位置是 哈希值再度经过 计算而得到的位置
同样的位置并不能代表哈希值一样
补充
关于情况2和情况3
此时key1-value1和原来的数据以链表的方式存储
在不断的添加过程中,会涉及到扩容问题,当超出扩容临界时(且要存放位置非空),则开始扩容。
默认的扩容方式:扩容为原来的2倍,并将所有的数据复制过来
默认的扩容方式:扩容为原来的2倍,并将所有的数据复制过来
JDK8
调用构造语句时
把当前加载因子 赋值为 0.75
DEFAULT_LOAD_FACTOR
HashMap的默认加载因子
默认的加载因子 = 0.75f
然后啥也没做了
底层不是Entry数组了,是Node数组
实际上这个类 确实 是实现了 Entry
存储数据时
put()
1. 调用hash()得到key的hash值
hash()
后面这一堆操作就是为了 降低冲突概率罢了(若以后有机会详细刨析一下)
调用 putValue():噩梦来啦哈哈哈哈哈
1. 先创建
空的Node数组 tab,
空的Node p,
声明遍历n、i
空的Node数组 tab,
空的Node p,
声明遍历n、i
2. 判断 (当前hashmap的底层数组是否为空?)
tab = 当前hashMap数组,是否为空?
n = tab(当前数组)的长度,是否为0?
tab = 当前hashMap数组,是否为空?
n = tab(当前数组)的长度,是否为0?
首次调用,是空
1. 那就调用 resize():
调用完后,返回一个我们创建好的数组
调用完后,返回一个我们创建好的数组
在 resize() 中,帮我们创建好底层数组
resize()
1. Node数组oldTab = 当前数组(目前还是null)
2. 当前oT(旧数组)是null?
是 => oldCap(旧容量) = 0
否 => oldCap(旧容量) = 当前oT容量
是 => oldCap(旧容量) = 0
否 => oldCap(旧容量) = 当前oT容量
3. oldThr(旧临界因子) = 临界因子(目前无值,是0)
4. newCap(新容量) ,newThr(新临界因子) = 0
5.如果oC > 0 ?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1.旧容量(0) >= MAXIMUM_CAPACITY(hashmap最大支持容量)?
MAXIMUM_CAPACITY
HashMap的最大支持容量
是
1. 加载因子 = Integer最大数值
2. 返回 旧数组
都大于最大值了,肯定存不下了,给你返回去
否
判断
(新容量 = 旧容量*2 且 < hashmap最大支持容量吗?)&&
(旧容量 >= 默认初始化容量吗?)
(新容量 = 旧容量*2 且 < hashmap最大支持容量吗?)&&
(旧容量 >= 默认初始化容量吗?)
DEFAULT_INITIAL_CAPACITY
默认初始化值
是
新临界因子 = 旧临界因子 * 2
否
出去
这一步过后,新容量*2,临界因子也*2
6. 老临界因子 > 0?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. 新容量 = 老临界因子
这步我没看懂,以后有机会来,因为目前不会走到这里面
7. 上面两个绿色判断都不满足?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. 新容量 = 默认初始容量 = 16
DEFAULT_INITIAL_CAPACITY
默认初始化值
2. 新临界因子 = (默认加载因子 * 默认初始容量)的整数
12 0.75 * 16
12 0.75 * 16
DEFAULT_INITIAL_CAPACITY
默认初始化值
DEFAULT_LOAD_FACTOR
HashMap的默认加载因子
8. 新临界因子是0吗?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. ft = 新容量 * 加载因子
2. 新临界因子 = 一堆操作后的某个值
因为没进来,我就不想解了
9. 临界因子 = 新临界因子(12)
10. 然后我们创建 容量为新容量(16)的Node数组 = newTab
11. 判断我们的 旧数组是不是空?
是 => 下一步
否 => 进来
是 => 下一步
否 => 进来
这里就是一系列把 旧数组数据 搬到 新数组中的操作
以后无聊可以来详解一下
以后无聊可以来详解一下
12. 返回我们创建好的 newTab
2. tab = 刚创建好的数组
n = 创建好的数组长度
n = 创建好的数组长度
3. (判断要存储位置是否无值,无值直接存)
tab目前位置所对应的Node 赋给了 p,
并且判断 p 是否为空?
是 => 进来
否 =>下一步
tab目前位置所对应的Node 赋给了 p,
并且判断 p 是否为空?
是 => 进来
否 =>下一步
1. 创建新Node(存入要存的key-value)到底层数组tab中 的 i 位置
添加成功!!!!!!!!!!!!!!!!!!!!!-- 情况1
添加成功!!!!!!!!!!!!!!!!!!!!!-- 情况1
4. 上面判断为否?
是 => 进来
是 => 进来
说明你要存储的位置 有值
1. 创建Node e,k
2. 判断 (你当前要存的key是否与当前位置所存在的key相同?)
当前位置值的哈希值是否与 你要存的 哈希是否相同? &&
k(当前位置的key) 是否 与你传入的 key 相同? ||
(key 不为空 && 调用equal()判断k是否与key相等?)
是 => 进来
否 => 下一步
当前位置值的哈希值是否与 你要存的 哈希是否相同? &&
k(当前位置的key) 是否 与你传入的 key 相同? ||
(key 不为空 && 调用equal()判断k是否与key相等?)
是 => 进来
否 => 下一步
你要是一样,我就取出当前位置的Node给e,
然后到 👉第5步,进行替换
然后到 👉第5步,进行替换
1. e = 当前位置底层数组对应的 Node
3. 判断 当前p是否是TreeNode类对象?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
这里就是说你是不是红黑树,是我就调用这种方法
1. 调用putTreeVal(),把当前p,数组,hash,key-value都存进去,然后把返回值赋给e
4. 上面两步都不成立?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
说明当前要存储的哈希值 与 当前存储位置的哈希值不相等,且不是红黑树
注意:是与第一个的哈希值不相等哟。因为存放一个位置可能链了好多个,所以开始了for循环
1. for循环遍历
遍历 初始值 binCount = 0
开始遍历
1. 判断当前位置p的next有没有值?
每次把 p的next 赋给 e(!!)
无 => 进来
有 => 下一步
每次把 p的next 赋给 e(!!)
无 => 进来
有 => 下一步
也就是说目前他存储的位置的next没有值了,所以我要存这里
1. 创建新Node(存入我们想要存放的key-value...)放到p.next
添加成功 !!!!!!!!!!!!!!!!! -- 情况 2
添加成功 !!!!!!!!!!!!!!!!! -- 情况 2
这就是 七上八下 的 八下
下面这步就是 如果 我们在Node[某位置]链了达到了8条,那么我们就要把当前位置的链式结构换成红黑树结构
2. 上面添加完成以后,我们还要判断下当前的 binCount 是否大于等于 红黑树临界容量(8)?
大于等于 => 有事,我们把这条链改成红黑树结构了,进来调用treeifyBin()方法,传入底层数组tab,和目前要存储的hash
小于 => 没事,继续下一步
大于等于 => 有事,我们把这条链改成红黑树结构了,进来调用treeifyBin()方法,传入底层数组tab,和目前要存储的hash
小于 => 没事,继续下一步
TREEIFY_THRESHOLD
Bucket中链表长度大于该默认值,转化为红黑树
treeifyBin()
1. 初始化 n,index,Node e
2. 判断 (当前数组长度是否大于被树化的最小容量?)
当前底层数组tab为空不?||
n = tab的长度 < Node被树化时最小的hash表容量吗?
小于 => 进来
大于 => 下一步
当前底层数组tab为空不?||
n = tab的长度 < Node被树化时最小的hash表容量吗?
小于 => 进来
大于 => 下一步
MIN_TREEIFY_CAPACITY
Bucket中的Node被树化时最小的hash表容量
当Bucket中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,
此时应执行resize操作扩容这个MIN_TREEIFY-CAPACITY的值至少是TREEIFY_THRESHOLD的4倍
此时应执行resize操作扩容这个MIN_TREEIFY-CAPACITY的值至少是TREEIFY_THRESHOLD的4倍
什么意思?
其实就是 我们单个位置链数以达到8了,我们想变成红黑树结构之前,还需要判断表容量
表容量默认16,我们至少要让表容量在64才可以变成红黑树结构,不然还是只能先扩容
表容量默认16,我们至少要让表容量在64才可以变成红黑树结构,不然还是只能先扩容
我们单个位置链数以达到8了,
我们想变成红黑树结构之前,还需要判断表容量表容量默认16,
我们至少要让表容量在64才可以变成红黑树结构,不然还是只能先扩容
我们想变成红黑树结构之前,还需要判断表容量表容量默认16,
我们至少要让表容量在64才可以变成红黑树结构,不然还是只能先扩容
1. 那就调用 resize():
调用完后,返回一个我们扩容好的数组
调用完后,返回一个我们扩容好的数组
这时候进入 resize() 中,帮我们扩容
resize()
1. Node数组oldTab = 当前数组(目前有数了,容量16)
2. 当前oT(旧数组)是null?
是 => oldCap(旧容量) = 0
否 => oldCap(旧容量) = 当前oT容量 = 16
是 => oldCap(旧容量) = 0
否 => oldCap(旧容量) = 当前oT容量 = 16
3. oldThr(旧临界因子) = 临界因子(目前无值,是0)
4. newCap(新容量) ,newThr(新临界因子) = 0
5.如果oC > 0 ?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1.旧容量(0) >= MAXIMUM_CAPACITY(hashmap最大支持容量)?
MAXIMUM_CAPACITY
HashMap的最大支持容量
是
1. 加载因子 = Integer最大数值
2. 返回 旧数组
都大于最大值了,肯定存不下了,给你返回去
否
判断
(新容量 = 旧容量*2 且 < hashmap最大支持容量吗?)&&
(旧容量 >= 默认初始化容量吗?)
(新容量 = 旧容量*2 且 < hashmap最大支持容量吗?)&&
(旧容量 >= 默认初始化容量吗?)
DEFAULT_INITIAL_CAPACITY
默认初始化值
是
新临界因子 = 旧临界因子 * 2
否
出去
这一步过后,新容量*2,临界因子也*2
6. 老临界因子 > 0?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. 新容量 = 老临界因子
7. 上面两个绿色判断都不满足?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. 新容量 = 默认初始容量 = 16
DEFAULT_INITIAL_CAPACITY
默认初始化值
2. 新临界因子 = (默认加载因子 * 默认初始容量)的整数
12 0.75 * 16
12 0.75 * 16
DEFAULT_INITIAL_CAPACITY
默认初始化值
DEFAULT_LOAD_FACTOR
HashMap的默认加载因子
8. 新临界因子是0吗?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
1. ft = 新容量 * 加载因子
2. 新临界因子 = 一堆操作后的某个值
因为没进来,我就不想解了
9. 临界因子 = 新临界因子
10. 然后我们创建 容量为新容量(32)的Node数组 = newTab
11. 判断我们的 旧数组是不是空?
是 => 下一步
否 => 进来
是 => 下一步
否 => 进来
这里就是一系列把 旧数组数据 搬到 新数组中的操作
以后无聊可以来详解一下
以后无聊可以来详解一下
12. 返回我们创建好的 newTab
3. 判断 (hash对应的tab存储位置 为空吗?)
e = tab对应要存储位置的Node,为空吗?
不为空 => 进来
为空 => 下一步
e = tab对应要存储位置的Node,为空吗?
不为空 => 进来
为空 => 下一步
1. 创建 TreeNode hd 为null,tl也为null
2. do while循环
开始遍历
这里就是 把原数组链的方式 转成 红黑树的方式
没讲了,老师没讲到这里,以后有机会详解把,目前了解到这里就差不多了
(有下一个不,有就继续遍历)
遍历条件:
e = e.next,
当e的next 是空时,结束循环
遍历条件:
e = e.next,
当e的next 是空时,结束循环
2. 判断 (你当前要存的key是否与当前位置的next所存在的key相同?)
当前位置的next的值的哈希值是否与 你要存的 哈希是否相同? &&
k(当前位置的next的key) 是否 与你传入的 key 相同? ||
(key 不为空 && 调用equal()判断k是否与p.next.key相等?)
是 => 进来
否 => 下一步
当前位置的next的值的哈希值是否与 你要存的 哈希是否相同? &&
k(当前位置的next的key) 是否 与你传入的 key 相同? ||
(key 不为空 && 调用equal()判断k是否与p.next.key相等?)
是 => 进来
否 => 下一步
1. break,退出循环
这时候说明 在当前位置不断next时,找到了一个 哈希值与我们存储哈希值相同的
然后到 👉第5步,进行替换
然后到 👉第5步,进行替换
3. p = e
进到这步说明 还有next,且next值不同
那么让 p = e,让p=p.next的意思
继续执行循环
那么让 p = e,让p=p.next的意思
继续执行循环
每次 ++
5. e 不为空吗?
是 => 进来
否 => 下一步
是 => 进来
否 => 下一步
若为 要存储的key 和 存储位置的key 相同,就会走到这步
把 旧的value 替换成了 新的value
把 旧的value 替换成了 新的value
1. 把老value存到 oldVlaue中
2. 判断
非onlyIfAbsent(传过来是false) ||
老数据为空吗?
是 => 进来
否 => 下一步
非onlyIfAbsent(传过来是false) ||
老数据为空吗?
是 => 进来
否 => 下一步
1. 把 value 存入了e.value 中
替换成功!!!!!!!
替换成功!!!!!!!
3. 不知干啥
4. 返回老value
6. modCount ++
7. 判断 ++size 后 > 扩容因子不?
是 => 调用resize()方法扩容
否 => 下一步
是 => 调用resize()方法扩容
否 => 下一步
8. 调用 这个啥方法我也不清楚,以后解释
9. 返回 空
老师总结
相较于 JDK7 在底层实现方面的不同
new HashMap()
底层没有创建一个长度为16的数组
底层的数组是:Node[],而非Entry[]
首次调用put()方法时,底层创建长度为16的数组
底层结构
JDK7
数组+链表
JDK8
数组+链表+红黑树
当数组的某一个索引位置上的元素以链表形式存在的元素个数 > 8 且当前数组的长度 > 64 时,
此时此索引位置上的所有数据改为使用红黑树存储
此时此索引位置上的所有数据改为使用红黑树存储
形成链表时,七上八下
JDK7
新的元素 next 指向 旧的元素
JDK8
旧的元素 next 指向 新的元素
源码中重要的常量
DEFAULT_INITIAL_CAPACITY
HashMap的默认容量
默认16
MAXIMUM_CAPACITY
HashMap的最大支持容量
DEFAULT_LOAD_FACTOR
HashMap的默认加载因子
默认 0.75
为啥是0.75,这个值是影响着 大概你存到多少数据时 开始准备扩容的
太大,可能你同一个位置会出现链表太多
太小,可能你压根没存多少就扩容了
太大,可能你同一个位置会出现链表太多
太小,可能你压根没存多少就扩容了
TREEIFY_THRESHOLD
Bucket中链表长度大于该默认值,转化为红黑树
默认 8
UNTREEIFY_THRESHOLD
Bucket中红黑树存储的Node小于该默认值,转化为链表
MIN_TREEIFY_CAPACITY
Bucket中的Node被树化时最小的hash表容量
当Bucket中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,
此时应执行resize操作扩容这个MIN_TREEIFY-CAPACITY的值至少是TREEIFY_THRESHOLD的4倍
此时应执行resize操作扩容这个MIN_TREEIFY-CAPACITY的值至少是TREEIFY_THRESHOLD的4倍
默认 64
table
存储元素的数组,总是2的n次幂
entrySet
存储具体元素的集
size
HashMap中存储的键值对的数量
modCount
HashMap扩容和结构改变的次数
threshold
扩容的临界值
容量 * 填充因子 =
默认 16 * 0.75 = 12
loadFactor
填充因子
DEFAULT_INITIAL_CAPACITY
默认初始化值
TreeMap
概念
保证按照添加的key-value对进行排序,实现排序遍历
此时考虑key的自然排序或定制排序
底层
红黑树
可进行自动排序
前提:向TreeMap中添加的数据,要求是相同类的对象
两种排序方式
自然排序(实现Comparable接口)
比较两个对象是否相同的标准
compareTo()返回0
而不是equals()
定制排序(Comparator)
比较两个对象是否相同的标准
compare()返回0
而不是equals()
使用方式
声明Comparator对象,并重写compare()方法
最后在声明 TreeMap 时,把Comparator对象传参定义就好。
TreeMap map = new TreeMap(com);
TreeMap map = new TreeMap(com);
实例
Hashtable
概念
作为古老的实现类,JDK1.0 出现
线程安全的,效率低
实现原理和hashMap相同,功能相同
不可 存储null的key和value
强行存会报空指针异常
Properties
常用来处理配置文件
概念
Properties类 是 Hashtable 的子类,该对象用于处理属性文件
Properties 的 key 和 value 都是字符串类型
因为属性文件里的key、value都是字符串类型
方法
getProperty(String key)
取出 key 对应的 value
实例
涉及到流的部分就不细讲多余操作了,详情细则去IO流那边
多说一句
若是你在idea中读取properties时,里面有中文,但是读取出来成了乱码
可能是以下没有设置
Settings -> Editor -> File Encodings 右下方,倒数第二格,请把右边勾勾勾上谢谢
setProperty(String key, String value)
存储 key - value
Entry接口
概念
用于存储键值对
方法
常用 getkey()、getValue()
Collections工具类
概念
java.util.Collections 是一个包装类
一个可以操作 Set、List 和 Map 等集合的工具类
提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
题外话
操作数组的工具类叫啥?
Arrays
方法
排序操作
reverse(List)
反转List中元素的顺序
PS:无序的集合你就别反转了,有个锤子用
shuffle(List)
对List中集合元素进行随机排序
PS:每次随机都不一样噢
sort(List)
根据元素的字然顺序对指定List集合元素按升序排序
PS:看到这个懂得啦,这个元素想要排序记得实现 Comparable接口 哟
sort(List, Comparator)
根据指定的Comparator产生的顺序对List集合元素进行排序
swap(List, int, int)
将指定list集合中的i处元素和j处元素进行交换
实例
编译
同步控制
概念
Collection 类中 提供了多个 synchronizedXxx() 方法,
该方法可 将 指定集合 包装成 线程同步的集合
从而解决多线程并发访问集合时的线程安全问题
该方法可 将 指定集合 包装成 线程同步的集合
从而解决多线程并发访问集合时的线程安全问题
到底是怎样实现 转成 线程安全的?
这里我们用 synchronizedList举例
当你调用 这个方法后,主要看他又 调用了 SynchronizedList 的构造方法,传入参数list
他调用该构造方法,把传入的 list 存入了他的属性中
内部类 SynchronizedList 的属性 list,负责存储list
那么关键点来了,他这个类里重写了很多 同步方法。(图片只截了部分)
所以才能实现 线程安全
所以才能实现 线程安全
synchronizedCollection(Collection c)
synchronizedList(List l)
synchronizedMap(Map m)
synchronizedSet(Set s)
synchronizedSortedMap(SortedMap sM)
synchronizedSortedSet(SortedSet sS)
其他常用
Object max(Collection)
根据元素的 自然排序,返回给定集合中最大元素
Object max(Collection, Comparator)
根据 Comparator 给定的 定制排序,返回给定集合中最大元素
Object min(Collection)
根据元素的 自然排序,返回给定集合中最小元素
Object min(Collection, Comparator)
根据 Comparator 给定的 定制排序,返回给定集合中最小元素
int frequency(Collection, Object)
返回指定集合中指定元素出现次数
实例
编译
void copy(List dest, List src)
将src中的内容复制到dest中
注意!
这个不懂乱用很容易报错!
你要是直接这样调用copy(),很容易报异常
异常
为啥呢?
看下这个copy()方法原理
注意第一个判断
要求 srcSize > dest.size()
否则就会抛异常
否则就会抛异常
也就是说,你的源集合,里面包含的元素 一定要大于 copy过来的目标集合
不然就报错
不然就报错
懂了原理之后:
我们如果想调用copy(),就要注意一下在copy()之前的操作
我们如果想调用copy(),就要注意一下在copy()之前的操作
创造少了元素
报错
创造多了元素
多出来,后面的元素不会被覆盖到
正确操作
例如这样子,就能完美创建刚好合适的元素大小
boolean replace(List list, Object oldVal, Object newVal)
使用新值 替换 List对象 的所有旧值
JDK5新特性 — 泛型
概念
泛型:标签
从 JDK1.5 后,Java引入了“参数化类型(Parameterized type)”的概念
允许我们创建集合时指定集合元素的类型
Collection<E>、List<E>、ArrayList<E>...
这个 <E> 就是 类型参数(类型实参),即 泛型
这个 <E> 就是 类型参数(类型实参),即 泛型
为何诞生
解决元素存储的安全性问题
你只想存String,结果有个傻冒存了个Integer
解决获取元素时,需要类型强制转换的问题
当集合没有泛型时
图示
实例
就像这样子,2种问题都有可能出现
当集合有泛型时
使用
实例1
若不使用,可能就会这样子
实例2
实例3
题外话:
(思考)这里调用Entry时,为啥前面要“Map.”
(思考)这里调用Entry时,为啥前面要“Map.”
其实因为Entry是Map类内部定义的接口
所以必须要Map.Entry来调用
除非
导入了Map,就可以直接Entry
JDK7新特性 — 类型推断
这时候,构造那边可以省略泛型类型。
自定义泛型 类
3种情况,下面三个我不推荐你看了,我讲不清楚。
怕把你绕了,建议直接看总结
怕把你绕了,建议直接看总结
情况1:当前类使用泛型
带泛型的类
实例
情况2:当前类继承泛型类(已指定类型)
不带泛型的类
如果是子类继承了定义了泛型的类,
但在继承时指定了 泛型类型,则该子类非带泛型类
但在继承时指定了 泛型类型,则该子类非带泛型类
实例
情况3:当前类继承泛型类(未指定类型)
带泛型的类
实例
自定义泛型 方法
这样是泛型方法吗?
不是呢
这里的T使用的是类定义的泛型,而非方法定义的泛型
怎样才算方法定义泛型
这样才算
这样也可
静态也可
怎样使用自定义泛型 方法
传参那一刻,决定了 泛型类型
其他
泛型在继承方面的体现
如
虽然 类A 是 类B 的父类,但是G<A> 和 G<B> 二者不具备子父类关系,二者是并列关系
补充
类A 是 类B 的父类,A<G> 是 B<G> 的父类
通配符的使用
类型通配符:?
类A 是 类B 的父类,G<A> 和 G<B> 是没有关系的,二者共同的父类是 G<?>
证明
使用实例
那么... 对于 G<?> 来说我还能操作吗?
以List举例
添加
不能向其内部添加数据,除了添加null以外
其实每个List都能加null,所以都可以
获取
允许读取数据,取得类型为Object
每个List父类都是Object,所以取出来直接是Object
有限制的通配符
通配符的上限:
? extend A
? extend A
G<? extend A> 可以作为 G<A> 和 G<B> 的父类,其中B是A的子类
范围:(-∞,A]
实例
读取数据
写入数据
? extend Comparable
只允许泛型为实现Comparable接口的实现类的引用调用
通配符的下限:
? super A
? super A
G<? super A> 可以作为 G<A> 和 G<B> 的父类,其中B是A的父类
范围:[A,+∞)
实例
写入数据
读取数据
IO(输入输出)
File类的使用
概念
java.io.File
File类的一个对象,代表一个文件或一个文件目录
想要再Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,
但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录
但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录
File类中涉及关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,
并为涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用IO流来完成。
并为涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用IO流来完成。
后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的“终点”
路径分隔符
路径中的每级目录之间用一个 路径分隔符 隔开
路径分隔符和系统有关
windows 和 DOS 系统默认使用 “\”来表示
UNIX 和 URL 使用“/”来表示
Java程序支持跨平台运行,因此路径分隔符要慎用
为了解决这个隐患,File类提供了一个常量
为了解决这个隐患,File类提供了一个常量
public static final String separator
根据操作系统,动态的提供分隔符
举例
常用构造器
public File(String pathname)
以Pathname为路径创建File对象,可以是绝对路径或者相对路径,
如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储
如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储
绝对路径
是一个固定的路径,从盘符开始
相对路径
是相对于某个位置开始
public File(String parent, String child)
以parent为父路径,child为子路径创建File对象
public File(File parent, String child)
根据一个父File对象和子文件路径创建File对象
常用方法
File类的获取功能
public String getAbsolutePath()
获取绝对路径
public String getPath()
获取路径
public String getName()
获取名称
public String getParent()
获取上层文件目录路径
若无,返回null
public long length()
获取文件长度(即:字节数)
不能获取目录的长度
public long lastModified()
获取最后一次的修改时间
public String[] list()
获取指定目录下的所有文件或者文件目录的名称数组
实例
若指定目录不存在,报空指针异常
public File[] listFiles()
获取指定目录下的所有文件或者文件目录的File数组
实例
若指定目录不存在,报空指针异常
硬盘中不存在 /
/ 硬盘中存在
/ 硬盘中存在
File类的重命名功能
public boolean renameTo(File dest)
把文件重命名为指定的文件路径
比如
file1.renameTo(file2)
要想保证返回true,
需要 file1 在硬盘是 存在 ,
且 file2 不能 再硬盘中 存在
需要 file1 在硬盘是 存在 ,
且 file2 不能 再硬盘中 存在
File类的判断功能
public boolean isDirectory()
判断是否是 文件目录
public boolean isFile()
判断是否是 文件
public boolean exists()
判断是否 存在
public boolean canRead()
判断是否 可读
public boolean canWrite()
判断是否 可写
public boolean isHidden()
判断是否 隐藏
注意
当判断的File不存在时,以上功能返回都是false
File类的创建功能
public boolean createNewFile()
创建文件
若文件存在,则不创建,返回false
实例
public boolean mkdir()
创建文件目录
如果此 文件目录存在,就 不创建 了
如果此 文件目录的上层目录不存在,也 不创建
如果此 文件目录的上层目录不存在,也 不创建
实例
public boolean mkdirs()
创建文件目录
如果上层文件目录不存在,一并创建
实例
注意事项
如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目路径下
File类的删除功能
public boolean delete()
删除文件或者文件夹
删除事项
Java中的删除不走回收站
要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录
实例
在开发中 特殊注意点
IDEA中
如果大家开发使用JUnit中的单元测试方法测试,相对路径即为当前Module下
如果大家使用main()测试,相对路径即为当前Project下
Eclipse中
不管使用单元测试方法还是使用main()测试,相对路径都是当前Project下
IO流原理 及 流的分类
原理
I/O是Input/Output的缩写,用于处理设备之间的数据传输
如
读写文件
网络通信
等
Java程序中,对于数据的输入/输出操作以“流(stream)”的方式进行
java.io包下提供了各种“流”类和接口,
用以获取不同种类的数据,并通过 标准的方法 输入或输出数据
用以获取不同种类的数据,并通过 标准的方法 输入或输出数据
输入输出
输入input
读取外部数据(磁盘..)到程序(内存)中
输出output
将程序(内存)输出到磁盘...中
如图
题外话
谷歌I/O
分类
Java的IO流共涉及40多个类,实际上费菜规则,都是从如下4个抽象集类派生的
由着四个类派生出来的子类名称都是以其父类名称作为子类名后缀
按照操作数据单位
字节流(8 bit)
InputStream
OutpetStream
字符流(16bit)
Reader
Writer
按照数据流的流向
输入流
InputStream
Reader
输出流
OutpetStream
Writer
按流的角色
节点流
处理流
IO流体系
重点
抽象集类 节点流(或文件流) 缓冲流(处理流的一种)
InputStream FileInputStream BufferedInputStream
OutputStream FileOutputStream BufferedOutputStream
Reader FileReader BufferedReader
Writer FIleWriter BufferedWriter
InputStream FileInputStream BufferedInputStream
OutputStream FileOutputStream BufferedOutputStream
Reader FileReader BufferedReader
Writer FIleWriter BufferedWriter
图示
节点流
直接操作特定对象(文件...)读取数据
字符流
构造器
FileXxx(File f)
FileXxx(String s)
s
文件路径
其实底层就是帮我们根据s路径new了个File来给FileReader/Writer
源码
FileWriter(String fileName, boolean append)
append
是 添加操作吗?
false
为覆盖操作
默认值
ture
为添加操作
方法
读入
步骤
File类的实例化
FileReader流的实例化
读入的操作
流资源的关闭
read()
返回 读入的 一个字符(int类型)
记得转成char
如果达到文件末尾,返回 -1
实例
只写操作,抛异常那些自己注意
推荐写法
对于数据读入可以这么写
read(char[] cbuf)
返回 读到的 字符数,同时将 读到的 字符 赋给 char[] c
如果达到文件末尾,返回 -1
实例
写出
步骤
提供File类的对象,指明写出到的文件
提供FileWriter的对象,用于数据的写出
写出操作
流资源的关闭
注意点
输出操作,对应的 File 可以不存在
并不会报异常
File对应的硬盘中的 文件是否存在
不存在
在输出过程中,会自动创建此文件
存在
流使用的构造器是:FileWriter(file, false) / FileWriter(file)
对原有文件 进行覆盖
流使用的构造器是:FileWriter(file, true)
不会对原有文件覆盖,而是在原有文件基础上 追加内容
FileWriter(File f, boolean append )
append
表示是否为添加操作
true
添加操作
false
默认值
重写操作
实例
write(char[] cbuf, 0, len)
注意
异常处理
为了保证流资源一点可以执行关闭操作。需要用 try-catch-finally 处理
推荐写法
流的关闭操作最好这么写
为什么判断空?
要是文件压根不存在,你这调用close直接空指针异常
读入文件一定要存在,否则会报 FileNotFoundException
不能用字符流来处理图片等字节数据
字节流(或文件流)
构造器
FileXxxStream(File f)
FileXxxStream(String s)
s
文件路径
其实底层就是帮我们根据s路径new了个File来给File Input/Output Stream
FileOutputStream(String name, boolean append)
append
是 添加操作吗?
false
为覆盖操作
默认值
ture
为添加操作
源码
方法
读入
跟字符流读写差不多
换汤不换药,自己理解
换汤不换药,自己理解
read(byte[] buffer)
写出
跟字符流读写差不多
换汤不换药,自己理解
换汤不换药,自己理解
write(byte[] buffer, 0, len)
处理流
“套接”在已存在的流的基础上 操作特定对象(文件...)读取数据
BufferedInputStream 套在 InputStream 上
BufferedOutputStream 套在 OutputStream 上
BufferedReader 套在 Reader 上
BufferedWriter 套在 Writer 上
缓冲流
BufferedInputStream / BufferedOutputStream
方法
跟字符流读写差不多
也就是在节点流操作完后再包裹一层处理流罢了
换汤不换药,自己理解
也就是在节点流操作完后再包裹一层处理流罢了
换汤不换药,自己理解
操作字节流
读入
跟字节流读写差不多
换汤不换药,自己理解
换汤不换药,自己理解
read(char[] cbuf)
写出
跟字节流读写差不多
换汤不换药,自己理解
换汤不换药,自己理解
write(char[] cbuf, 0, len)
操作字符流
读入
跟字符流读写差不多
换汤不换药,自己理解
换汤不换药,自己理解
read(byte[] buffer)
readLine()
读取一行
实例
注意
readLine 读取所获取到的 data 不包括换行符
可使用newLine()方法搭配 或 “/n”
写出
跟字符流读写差不多
换汤不换药,自己理解
换汤不换药,自己理解
write(byte[] buffer, 0, len)
flush()
刷新缓冲区
就是缓冲区还没存满,也给你清了
newLine()
换行
可根据操作系统的不同底层设置换行符
说明
关闭外层流的同时,内层流也会自动的进行关闭
关于内层流的关闭,我们可以省略
flush()方法 可以刷新缓冲区
作用
提供流的读取、写入的速度
原因
内部提供了一个缓冲区
构造方法
默认缓冲区大小 8192 = 8 * 1024
转换流
概念
提供了在字节流和字符流之间的转换
很多时候我们使用转换流来处理文件乱码问题
实现编码和解码的功能
图示
JavaAPI提供了两个转换流
InputStreamReader
将 InputStream 转换为 Reader
解码
字节、字节数组 --> 字符数组、字符串
构造器
用什么字符集 取决于 文件保存时 使用的字符集,否则可能会乱码哟
public InputStreamReader(InputStream in)
使用系统默认的字符集
每次读取一个字符,返回int(unicode字符唯一编码)
实例
源码说明
public InputStreamReader(InputStream in, String charsetName)
public InputStreamReader(InputStream in, Charset cs)
OutputStreamWriter
将 Writer 转换为 OutputStream
编码
字符数组、字符串 --> 字节、字节数组
实例
把字节流 转换为 字符流
举个例子(异常处理自己解决,这里只写具体过程)
字节流 转字符流(换编码)再转 字节流
举个例子(异常处理自己解决,这里只写具体过程)
System
标准输入、输出流
概念
System.in 和 System.out 分别代表了系统标准的输入和输出设备
System.in
标准的输入流
默认输入设备
键盘
System.out
标准的输出流
默认输出设备
显示器
System.in 的类型是 InputStream
System.out 的类型是 printStream
是OutputStream的子类
FilterOutputStream的子类
重定向
通过System类的setIn,setOut方法对默认设备进行改变
public static void setIn(InputStream in)
public static void setOut(PrintputStream out)
System属性
实例
读取键盘输入的字母转为大写输出,输入“e”退出程序
方式一:使用Scanner实现,调用next()返回一个字符串
方式二:使用System.in实现。
System.in --> 转换流 --> BufferedReader的readLine()
System.in --> 转换流 --> BufferedReader的readLine()
打印流
概念
实现将 基本数据类型 的数据格式转化为 字符串 输出
PrintStream
打印的所有字符都默认使用平台默认字符编码转换为字节
在需要写入 字符 而不是写入 字节 的情况下,应该使用 PrintWriter类
System.out 返回的是 PrintStream 的实例
PrintWriter
数据流
概念
为了更方便地操作Java语言的基本数据类型和String的数据,可以使用数据流
数据流有两个类
用于读取和写出基本数据类型、String类的数据
分别“套接”在InputStream 和 OutputStream 子类的流上
DataInputStream
注意点
读取不同类型的数据的顺序要与当初写入文件时,保存的数据的顺序一致!
方法
boolean readBoolean()
byte readByte()
char readChar()
float readFloat()
double readDouble()
short readShort()
long readLong()
int readInt()
String readUTF()
void readFilly(byte[] b)
实例
将文件中存储的 字符串、基本数据类型的变量 读取到内存中,保存到变量中
只写具体步骤,处理异常省略了,自己加。
DataOutputStream
方法
将 DataInputStream 方法的 read改成write 就是了
实例
将内存中的字符串、基本数据类型的变量写道文件中
只写具体步骤,处理异常省略了,自己加。
对象流
概念
用于存储和读取 基本数据类型数据 或 对象 的处理流
强大之处
可以把Java中的对象写入到数据源当中
把对象从数据源中还原回来
注意点
不能序列化或反序列化 static 和 transient 修饰的成员变量
如果需要让某个对象支持序列化机制,则必须让 对象所属的类 及 其属性 是可序列化的
ObjectOutputStream
序列化
用 ObjectInputStream类 读取 基本数据类型数据或对象机制
概念
允许把内存中的Java对象转换成与平台无关的二进制流
从而允许把这种二进制流持久地保存在磁盘上
或通过网络将这种二进制流传输到另一个网络节点
序列化是 RMI(Remote Method Invoke - 远程方法调用)过程的参数和返回值都必须实现的机制,而RMI 是 JavaEE 的基础
因此序列化机制是 JavaEE 平台的基础
好处
任何实现了 Serializable接口 的对象转化为字节数据,使其在保存和传输时可被还原
如果需要让某个对象支持序列化机制,
则必须让 对象所属的类 及 其属性 是可序列化的
则必须让 对象所属的类 及 其属性 是可序列化的
方式一:实现Serializable
1. 需要实现接口 Serializbale
该接口算是标识接口,什么内容也没有
也就是标识什么类可以序列化
2. 当前类提供一个全局常量:serialVersionUID(序列版本号)
serialVersionUID 干嘛的?
Serializable接口的注释也说明了,请提供一个静态的ID
概念
serialVersionUID 用来表明类的不同版本间的 serialVersionUID
兼容性
目的是以序列化对象进行版本控制,有关个版本是否兼容
若没有定义这个常量
他的值是Java运行时环境根据类的内部细节自动生成的
若此时实例变量做了修改,serialVersionUID 可能会发生变化,导致反序列化无法识别该对象
serialVersionUID 使用方式
效果如上
3. 其属性也必须保证是可序列化的(基本数据类型、string本可序列化)
方式二:实现Externalizable
否则,会抛出NotSerializableException异常
ObjectInputStream
反序列化
用 ObjectOutputStream类 保存 基本数据类型数据或对象机制
概念
当其他程序获取了这种二进制流,就可以恢复成原来的Java对象
serialVersionUID
反序列化时,JVM会把传来的字节流中得到serialVersionUID与本地相应实体类的serialVersionUID进行比较
相同
认为一致
可反序列化
不相同
可能出现序列化版本不一致的异常:InvalidCastException
实例
若序列化了多个对象,那也要按照对应顺序来读取
如上
随机存取文件流
RandomAccessFile
概念
RandomAccessFile 声明在 java.io 包下
直接继承 java.lang.Object类
实现了 DataInput、DataOutput接口
意味着可以读也可以写
支持“随机访问”的方式
程序可以直接跳到文件的任意地方来读、写文件
支持只访问文件的部分内容
可以向已存在的文件后追加内容
RandomAccessFile对象
包含一个记录指针
用以标识当前读写的位置
可以自由移动记录指针
方法
long getFilePointer()
获取文件记录指针的当前位置
void seek(long pos)
将文件记录指针定位到 pos 位置
拓展
我们可以用该类实现一个多线程断点下载的功能。
构造器
mode参数
指定RandomAccessFile 的访问模式
r
以只读方式打开
rw
打开以便读取和写入
rwd
打开以便读取和写入
同步文件内容的更新
rws
打开以便读取和写入
同步文件内容的更新 和 元数据的更新
注意点
如果模式为只读r,则不会创建文件,而是会读取一个已经存在的文件
文件若不存在,则会出现异常
如果模式为rw读写,当文件不存在时,会创建文件
若文件存在,则不会创建
JDK1.6上面写的每次write数据时,
“rw”模式,数据不会立即写到硬盘中;
“rwd”模式,数据会被立即写入硬盘。
“rw”模式,数据不会立即写到硬盘中;
“rwd”模式,数据会被立即写入硬盘。
若写数据过程中发生异常,
“rw”模式则会全部丢失;
“rwd”模式中已被write的数据会被保存到硬盘
“rw”模式则会全部丢失;
“rwd”模式中已被write的数据会被保存到硬盘
如果写出到的文件存在,则会对原有的文件内容进行覆盖(默认情况下,从头覆盖)
public RandomAccessFile(File file, String mode)
public RandomAccessFile(String name, String mode)
实例
复制文件
覆盖字符
编译前文件
编译后文件
如果写出到的文件存在,则会对原有的文件内容进行覆盖(默认情况下,从头覆盖)
指定位置覆盖字符
编译前文件
编译后文件
字符插入
可以通过“相关的操作”,实现RandomAccessFile“插入”数据的效果
编译前文件
编译后文件
NIO、NIO2
NIO
说实话,没怎么用,用也用NIO.2
概念
NIO(New IO,Non-Blocking IO)
Java 1.4 引入的一套新的IO API,可以替代标准的Java IO API
NIO将以更加高效的方式进行文件的读写操作
提供了两套NIO
一套是针对标准输入输出的NIO
一套是网络编程NIO
Java.nio.channels.Channel
FileChannel
处理本地文件
SocketChannel
TCP网络编程的客户端的Channel
ServerSocketChannel
TCP网络编程的服务器端的Channel
DatagramChannel
UDP网络编程中发送端和接收端的Channel
NIO.2
概念
随着 JDK7 发布,Java对NIO进行了极大拓展
因此我们称它为NIO.2
Path
接口
概念
早期Java只提供File,但File功能局限,性能不高,且出错只返回false,不提供异常信息
因此 NIO.2 引入了Path接口
代表一个平台无关的平台路径
描述了目录文件结构中文件的位置
可以看作是File类的升级版本,实际引用的资源也可以不存在
以前IO
Java7中
方法
Paths
工具类
概念
包含了两个返回Path的静态工厂方法
方法
获取 Path对象
static Path get(String first, String ... more)
用于将多个字符串连成路径
static Path get(URI uri)
返回指定uri对应的path路径
实例
Files
工具类
概念
java.nio.file.Files
用于操作文件或目录的工具类
包含了大量静态的工具方法来操作文件
方法
常用方法
用于判断
用于操作内容
拓展:字符编码
编码表
概念
计算机只能识别二进制数据
字符对应的二进制表
常见的编码表
ASCII
没过的标准信息交换码
用一个字节的7位可以表示
ISO8859-1
拉丁码表
欧洲码表
用一个字节的8位表示
GB2312
中国的中文编码表
对多两个字节编码所有字符
GBK
中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
Unicode
国际标准码,融合了目前人类使用的所有子字符
为每个字符分配唯一的字符码
所有的文字都用2个字节来表示
UTF-8
变长的编码方式,可用1-4个字节来表示一个字符
补充
内存操作流
概念
内存操作流是为了临时处理数据而设置的
ByteArrayOutputStream
概念
实现了一个输出流,其中的数据被写入一个 byte 数组
缓冲区会随着数据的不断写入而自动增长
可使用 toByteArray() 和 toString() 获取数据
关闭 ByteArrayOutputStream 无效
此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException。
实例
可以看服务端 读取方式三 的那段代码
ByteArrayInputStream
概念
包含一个内部缓冲区,该缓冲区包含从流中读取的字节
内部计数器跟踪 read 方法要提供的下一个字节
关闭 ByteArrayInputStream 无效
此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException。
JDBC
什么是JDBC
JDBC常用API
Driver接口
DriverManager接口
Connection接口
Statement接口
PreparedStatement接口
ResultSet接口
实现第一个JDBC程序
PreparedStatement对象
ResultSet对象
多线程
基本概念
程序
是指为完成特定任务、用某种语言编写的一组指令的集合。
即一段静态的代码,静态对象。
进程
是程序的一次执行过程,或是正在运行的程序。
是一个动态的过程
产生
存在
消亡
进程作为资源分配的单位
系统在运行时会为每个进程分配不同的内存区域
线程
是一个程序内部的一条执行路径
若一个进程同一时间并执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每一个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
拥有独立的 栈、程序计数器
共享 同一进程的 堆、方法区
共享 同一进程的 堆、方法区
他们从同一堆中分配对象,可以访问相同的变量和对象。
这就使得线程之间得通信更简便、高效。
但多个线程操作共享得系统资源可能就会带来安全的隐患。
一个 Java应用程序java.exe 只有有三个线程
main()主线程
gc()垃圾回收线程
异常处理线程
如果发生异常,会影响主线程
单核CPU 和 多核CPU 的理解
并行与并发
并行
多个CPU同时执行多个任务
如
多个人同时做不同的事情
并发
一个CPU(采用时间片)同时执行多个任务
如
秒杀、多个人做同一件事
使用多线程的优点
背景
以单核CPU为例,只是用单个线程先后完成多个任务,肯定比用多个线程来完成用的时间更短,为何还需要多线程呢?
优点
提高应用程序的相应。
对图形化界面更有意义,可增强用户体验
题号计算机系统CPU利用率
改善程序结构
将既长又复杂的进程分为多个线程,独立运行,利用理解和修改
线程的分类
他们在每个方面几乎都是相同的,唯一区别就是判断JVM何时离开
用户线程
如
main()
守护线程
服务 用户线程
通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成守护线程
若JVM中都是守护线程,当前JVM将退出
兔死狗烹,鸟尽弓藏
如
Java垃圾回收线程
线程的创建 和 使用
概述
Java 语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现
Thread类的特性
每个线程都是通过某个特定Thread对象的run()方法来完成操作的
经常把run()方法的主体称为线程体
通过Thread对象的start()方法来启动这个线程,而非直接调用run()
创建 及 使用
创建方式一:继承于Thread类
创建一个继承于Thread类的子类
重写Thread类的run()
将此线程执行的操作声明在run()中
创建Thread类的子类对象
通过此对象调用start()
启动线程
调用当前线程的run()
在此基础上 使用匿名子类的方式
创建一个继承于Thread类的子类
重写Thread类的run()
通过此对象调用start()
如
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " = " + i);
}
}
}
}.start();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " = " + i);
}
}
}
}.start();
创建方式二:实现Runnable接口
创建一个实现了Runnable接口的类
实现类去实现Runnable中的抽象方法:run()
创建实现类的对象
将此对象作为参数传递到了Thread类的构造器中,创建Thread类的对象
通过Thread类的对象调用start()
启动线程
调用当前线程的run()
调用了Runnable类型的target的run()
JDK5.0 新增 线程创建方式
创建方式三:实现Callable接口
概念
与使用Runnable相比,Callable功能更强大些
相比run()方法,可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
Future接口
可以对具体Runnable、Callable惹怒我的执行结果进行取消、查询是否完成、获取结果等
FutureTask是Future接口的唯一实现类
FutureTask同时实现了Runnable、Future接口
既可以作为Runnable被线程执行
也可以作为Future得到Callable的返回值
如何理解 实现Callable接口的方式创建多线程 比 实现Runnable接口创建多线程 的方式强大?
call() 可以有返回值
call() 可以抛出异常,被外面的操作捕获,获取异常的信息
Callable 是支持泛型的
使用
实现一个Callable的实现类
实现call()方法,将此线程需要执行的操作声明在call()中
创建Callable接口实现类的对象
将此Callable接口实现类的对象作为传递参数传递到FutureTask构造器中,创建FutureTask的对象
调用start(),启动线程
获取Callable中call方法的返回值
代码
// 1. 实现一个Callable的实现类
class A implement Callable {
// 2. 实现call()方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
//方法体
return 返回值
}
}
public class XXX {
main(){
// 3. 创建Callable接口实现类的对象
A a = new A();
// 4. 将此Callable接口实现类的对象作为传递参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask f = new FutureTask(a);
// 5. 调用start(),启动线程
new Thread(f).start();
try {
// 6. 获取Callable中call方法的返回值
// get()返回值就是 FutureTask构造器参数Callable实现类重写的call()的返回值
Object xxx = f.get();
} catch (XXX x) {
// 略
}
}
}
class A implement Callable {
// 2. 实现call()方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
//方法体
return 返回值
}
}
public class XXX {
main(){
// 3. 创建Callable接口实现类的对象
A a = new A();
// 4. 将此Callable接口实现类的对象作为传递参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask f = new FutureTask(a);
// 5. 调用start(),启动线程
new Thread(f).start();
try {
// 6. 获取Callable中call方法的返回值
// get()返回值就是 FutureTask构造器参数Callable实现类重写的call()的返回值
Object xxx = f.get();
} catch (XXX x) {
// 略
}
}
}
创建方式四:使用线程池
背景
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中
可以避免频繁创建销毁、实现重复利用
好处
提高响应速度
减少了创建新线程的时间
降低资源消耗(提高资源重用率)
重复利用线程池中线程,不需要每次都创建
便于线程管理
corePoolSize
核心池大小
maximumPoolSize
最大线程数
keepAliveTime
县城没有任务时最多保持多长时间后回中止
...
线程池相关API
JDK5.0 提供了线程池相关API
ExecutorService
真正的线程池接口
常见子类
ThreadPoolExecutor
常见方法
void execute(Runnable command)
Excecutors
代码
main() {
// 1. 提供指定线程数量的线程池
ExecutorService s = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service = (ThreadPoolExecutor)service;
// 设置线程池属性
service.setCorePoolSize(15);
....
// 2. 执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
s.execute(new 实现Runnable/Callable类());
// 3. 关闭连接池
s.shutdown();
}
// 1. 提供指定线程数量的线程池
ExecutorService s = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service = (ThreadPoolExecutor)service;
// 设置线程池属性
service.setCorePoolSize(15);
....
// 2. 执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
s.execute(new 实现Runnable/Callable类());
// 3. 关闭连接池
s.shutdown();
}
线程的调度
调度策略
时间片
抢占式
高优先级的线程抢占CPU
Java的调度方法
同优先级线程组成先进先出队列(先到先服务),使用时间片策略
对高优先级,先使用优先调度的抢占式策略
线程的优先级等级
MAX_PRIORITY
10
public final static int MAX_PRIORITY = 10
MIN_PRIORITY
1
public final static int MIN_PRIORITY = 1
NORM_PRIORITY
5
public final static int NORM_PRIORITY = 5
涉及的方法
geitPriority()
返回线程优先值
如
Thread.currentThread().getPriority()
setPriority(int newPriority)
改变线程的优先级
如
Thread.currentThread().setPriority(Thread.MAX_PRIORITY)
说明
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
Thread 类的有关方法
start()
启动当前线程
调用当前线程的run()
run()
通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
currentThread()
静态方法
返回执行当前代码的线程
在Thread子类中就是this,通常用于主线程和Runnable实现类
getName()
获取当前线程的名字
setName()
设置当前线程的名字
yield()
静态方法
释放当前cpu的执行权
join()
在线程a中调用线程b的join(),此时线程a就进入阻塞状态,指导线程b完全执行完后,线程a才结束阻塞状态
stop()
已过时
当执行此方法时,强制结束当前线程
sleep(long millitime)
静态方法
让当前线程“睡眠”指定的millitime毫秒
在指定的millitime毫秒时间内,当前线程时阻塞状态
isAlive()
判断当前线程是否存活
线程的生命周期及状态转换
在一个完整的生命周期中通常要经历如下的五种状态
状态的切换导致某个方法的执行,那么这个方法我们一般称为回调方法
而多线程中,我们讲的是调用方法而导致状态改变(非回调方法)
新建
当一个Thread类或子类的对象被声明并创建时,新手的线程对象处于新建状态
就绪
处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行
当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞
在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡
线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
五种状态关联图
如图
线程的同步
线程安全问题的理解
在Java中,我们通过同步机制,来解决线程的安全问题
问题
买票过程中,出现了重票、错票
出现的原因
当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
解决方案
当一个线程a在错做ticket的时候,其他线程不能参与进来。
指导线程a操作完ticket时,其他线程才可以开始操作ticket。
这种情况即使线程a出现了阻塞,也不能被改变
指导线程a操作完ticket时,其他线程才可以开始操作ticket。
这种情况即使线程a出现了阻塞,也不能被改变
同步监视器
俗称:锁
任何一个类的对象,都可以充当锁。
要求
多个线程必须要共用一把锁
释放锁的操作
当前线程的同步方法、同步代码块 执行结束
当前线程在同步代码块、同步方法中 遇到break、retrun 终止了该代码块、该方法的继续执行
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
当前线程在同步代码块、同步方法中执行了线程对象的 wait()方法,当前线程暂停,并释放锁
不会释放锁的操作
线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield()方法 暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的 suspend()方法 将该线程 挂起,该线程不会释放锁(同步监视器)
应尽量避免使用suspend()和resume()来控制线程
使用
方式一:同步代码块
代码
synchronized(同步监视器){
// 需要被同步的代码(操作共享数据的代码)
}
// 需要被同步的代码(操作共享数据的代码)
}
说明
操作共享数据的代码,即为需要被同步的代码
共享数据
多个线程共同操作的变量
比如
ticket就是共享数据
不能包含代码多了,也不能包含代码少了
同步监视器
俗称:锁
任何一个类的对象,都可以充当锁。
要求
多个线程必须要共用一把锁
可使用 this/当前类 来充当同步监视器
在继承Thread类创建多线程的方式
慎用this充当同步监视器,考虑使用当前类来充当同步监视器
如
Windows.class
在实现Runnable接口创建多线程的方式
可以使用this来充当同步监视器
方式二:同步方法
如果操作共享数据的段代码完整的声明在一个方法中,我们不妨将此方法声明为同步的
使用
在 方法中声明中 增添 synchronized 关键字
总结
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
非静态的同步方法
同步监视器是:this
静态的同步方法
同步监视器是:当前类本身
方式三:Lock锁
JDK 5.0 新增
通过显式定义同步锁对象来实现同步
同步锁使用 Lock 对象充当
概念
Java.util.concurrent.locks.Lock 接口时控制多个线程队共享资源进行访问的工具
锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁
ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义。
使用
实例化 ReentrantLock
调用锁定方法:lock()
调用解锁方法:unlock()
实例
class xxx implements Runnable {
// 1.实例化 ReentrantLock
private ReentrantLock lock = new ReentrantLock();
run方法() {
try {
// 2.调用锁定方法:lock()
lock.lock();
<中间的操作>
}finally {
// 3.调用解锁方法:unlock()
lock.unlock();
}
}
}
// 1.实例化 ReentrantLock
private ReentrantLock lock = new ReentrantLock();
run方法() {
try {
// 2.调用锁定方法:lock()
lock.lock();
<中间的操作>
}finally {
// 3.调用解锁方法:unlock()
lock.unlock();
}
}
}
注意点
若线程是实现Runnable的形式,声明ReentrantLock对象属性 默认都是同一个
但是 若是继承 Thread 类来实现,注意要保证调用 开锁解锁方法的 ReentrantLock对象 得是同一个
好处
同步的方式,解决了线程的安全问题
局限性
操作同步代码时,只能有一个线程参与,其他线程等待。相当于时一个单线程的过程,效率低。
线程的死锁问题
概念
不同的线程分别占用对方需要的同步资源不放弃,都在等对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
比如
线程1握着a锁等b
线程2握着b锁等a
线程2握着b锁等a
线程的通信
概念
多个线程交互
常用方法
wait()
一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器
面试题
sleep() 和 wait() 的异同?
相同点
一旦执行方法,都可以让线程进入阻塞状态
不同点
两个方法声明的位置不同
sleep()声明在 Thread类 中
wait()声明在 Object类 中
调用的范围不同
sleep() 可以在任何需要的场景 调用
wait() 只在同步代码块/同步方法中 调用
是否释放监视器
如果两个方法都是用在同步代码块或同步方法中时
sleep() 不会释放 同步监视器
wait() 会释放 同步监视器
notify()
一旦执行此方法,就会唤醒被wait的一个线程
如果有多个线程被wait,就会唤醒优先级高的那个线程
notifyAll()
一旦执行此方法,就会唤醒所有被wait的线程
说明
wait(),notify(),notifyAll() 三个方法必须使用在 同步代码块或同步方法(synchronized) 中
wait(),notify(),notifyAll() 三个方法的调用者必须是 同步代码块或同步方法中的同步监视器
否则
会出现IllegalMonitorStateException异常
wait(),notify(),notifyAll() 三个方法 是定义在 java.lang.Object类 中
线程通信的应用
枚举类与注解
枚举类
概念
类的对象只有有限个,确定的 我们称此类为枚举类
如
星期一、二...
性别男、女
春天、夏天...
未付款、已付款、已确认
什么时候建议使用?
当需要定义一组常量时,强烈建议使用枚举类
若枚举类中只有一个对象,则可以作为单例模式的实现方式
使用
自定义 枚举类
说明
jdk5.0之前
定义方式
声明枚举类对象的属性:private final修饰
私有化类的构造器,并给对象赋值
提供当前枚举类的多个对象:public static final修饰
其他诉求:获取枚举类对象的属性、重写toString()
举例
定义部分
class Xxx{
//1.声明Xxx对象的属性
private final String name;
//2.私有化类的构造器,并给对象赋值
private Xxx(String name){
this.name = name;
}
//3.提供当前枚举类的多个对象:public static final修饰
public static final Xxx x1 = new Xxx("XXX1");
public static final Xxx x2 = new Xxx("XXX2");
...
//4.其他诉求:获取枚举类对象的属性、重写toString()
...
}
//1.声明Xxx对象的属性
private final String name;
//2.私有化类的构造器,并给对象赋值
private Xxx(String name){
this.name = name;
}
//3.提供当前枚举类的多个对象:public static final修饰
public static final Xxx x1 = new Xxx("XXX1");
public static final Xxx x2 = new Xxx("XXX2");
...
//4.其他诉求:获取枚举类对象的属性、重写toString()
...
}
使用部分
main(){
Xxx x = Xxx.x1;
sout(x);
}
Xxx x = Xxx.x1;
sout(x);
}
关键字enum 定义枚举类
说明
定义的枚举类默认继承于 java.lang.Enum 类
jdk5.0
所有的枚举类都提供了一个 values() 方法,返回提供所有枚举对象构成的数据
枚举类的构造器只能使用 private 定义,而普通类的还可以用 public 修饰
定义方式
提供当前枚举类的对象
多个对象之间用“,”隔开,末尾对象“;”结束
相同部分声明基本删除
多个对象之间用“,”隔开,末尾对象“;”结束
相同部分声明基本删除
注意:枚举类的对象一定要声明在最上面
声明枚举类对象的默认属性:public static final修饰
私有化类的构造器,并给对象赋值
其他诉求:获取枚举类对象的属性、重写toString()
举例
定义部分
class Xxx{
//1.提供当前枚举类的对象多个对象之间用“,”隔开,末尾对象“;”结束;相同部分声明基本删除
x1("XXX1"),
x2("XXX2");
...
//1.声明Xxx对象的属性
private final String name;
//2.私有化类的构造器,并给对象赋值
private Xxx(String name){
this.name = name;
}
//4.其他诉求:获取枚举类对象的属性、重写toString()
...
}
//1.提供当前枚举类的对象多个对象之间用“,”隔开,末尾对象“;”结束;相同部分声明基本删除
x1("XXX1"),
x2("XXX2");
...
//1.声明Xxx对象的属性
private final String name;
//2.私有化类的构造器,并给对象赋值
private Xxx(String name){
this.name = name;
}
//4.其他诉求:获取枚举类对象的属性、重写toString()
...
}
class Ddd{
DD1,DD2,DD3;
}
DD1,DD2,DD3;
}
如果你就单纯定义不带任何属性的枚举对象,就是这么简单
使用部分
main(){
Xxx x = Xxx.x1;
sout(x);
}
Xxx x = Xxx.x1;
sout(x);
}
若不重写toString方法,默认打印属性名
Enum类
主要方法
values()
返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值
实例
编译
valueOf(String str)
可以把一个字符串转为对应的枚举类对象
要求
字符串必须是枚举类对象的“名字”
若不是,会出现运行时异常 IllegalArgumentException
实例
编译
toString()
返回当前枚举类对象常量的名称
图示
枚举类 实现接口
实现接口的情况
实现接口,再enum类中实现抽象方法
就是正常实现就行
让枚举类的对象分别实现接口中的方法
注意是枚举类
举例
interface YYY{
void AAA();
}
class Xxx implements YYY{
//1.提供当前枚举类的对象多个对象之间用“,”隔开,末尾对象“;”结束;相同部分声明基本删除
x1("XXX1"){
public void AAA(){ //x1对应方法体 };
},
x2("XXX2"){
public void AAA(){ //x2对应方法体 };
};
...
//1.声明Xxx对象的属性
private final String name;
//2.私有化类的构造器,并给对象赋值
private Xxx(String name){
this.name = name;
}
//4.其他诉求:获取枚举类对象的属性、重写toString()
...
}
void AAA();
}
class Xxx implements YYY{
//1.提供当前枚举类的对象多个对象之间用“,”隔开,末尾对象“;”结束;相同部分声明基本删除
x1("XXX1"){
public void AAA(){ //x1对应方法体 };
},
x2("XXX2"){
public void AAA(){ //x2对应方法体 };
};
...
//1.声明Xxx对象的属性
private final String name;
//2.私有化类的构造器,并给对象赋值
private Xxx(String name){
this.name = name;
}
//4.其他诉求:获取枚举类对象的属性、重写toString()
...
}
这样使用,每一个对象就可以对应 不同的实现方法
注解
概述
从JDK5.0新增,Java增加了对元数据(MetaData)的支持,也就是Annotation(注解)
Annotation其实就是代码里的特殊标记
作用
不是程序本身,可以对程序作出解释
和注释没什么区别
可以被其他程序(编译器等)读取
格式
@注释名
还可添加一些参数值
如@SupperessWarnings(value="unchecked")
在哪使用
可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明
我们可以通过反射机制编程实现对这些元数据的访问
杂谈
未来的开发模式都是基于注解的,这是趋势
一定程度上:框架 = 注解 + 反射 + 设计模式
实例
生成文档相关的注解
@author
表明开发该类模块的作者,多个作者之间使用“,”分割
@version
标明该模块的版本
@see
参考转向,也就是相关主题
@since
从哪个版本开始增加的
@param
对方法中某个参数的说明,如果没有参数就不能写
格式要求
@param 形参名 形参类型 形参说明
可以多个
@return
对方法返回值的说明,如果返回值类型是void就不能写
格式要求
@return 返回值类型 返回值说明
@exception
对方法可能抛出的异常进行说明,如果方法没有throws显式抛出的异常就不能写
格式要求
@exception 异常类型 异常说明
可以多个
在编译时进行格式检查(JDK内置的三个基本注解)
@Override
限定重写父类方法,该注解只能用于方法
@Deprecated
用于表示所修饰的元素(类、方法等)已过时
通常是因为所修饰的结构危险或存在更好的选择
@SupperessWarnings
抑制编译时的警告信息
与前两个注释有所不同,你需要添加一个参数才能正常使用,这些参数都是定义好的,选择性使用即可
包含
@SuppressWarning("all")
@SuppressWarning("unchecked")
@SuppressWarning("unchecked", "deprecation")
...
跟踪代码依赖性,实现替代配置文件功能
spring框架中关于“事务”的管理
自定义
使用@interface自定义注解时, 自动继承了 java.lang.annotation.Annotation 接口
分析
@interface 用来声明一个注解
格式:public @interface 注解名 {定义内容}
成员变量 以 无参方法形式 声明
方法名和返回值定义了该成员的名字和类型
类型
八种基本数据类型
String
Class
enum
Annotation
以及以上所有类型的数组
可指定默认值,使用 default关键字
若只有一个参数成员,则建议参数名为value
这样写,我们使用的时候可以省去(value = )
自定义注解必须配上注解的信息处理流程才有意义
举例
例一
定义
public @interface MyAnnotation {
String value();
//String value() default "hello";
}
String value();
//String value() default "hello";
}
使用
@MyAnnotation(value = "hello")
@MyAnnotation("hello")
这种情况,value就可省略了
例二
定义
// 这里去掉了public,因为每个编译单元(文件)都只能有一个public类
// 表示我们的注解可以用到什么地方
@Target(value = {ElementType.METHOD,ElementType.TYPE})
// 表示我们的注解在什么地方还有小
@Retention(value = RetentionPolicy.RUNTIME)
// 表示是否将我们的注解生成在JavaDOC中
@Documented
// 子类可以继承父类的注解
@Inherited
@interface MyAnnotation {
// 注解的参数:参数类型+参数名()
String name() default "abc";
int age() default 0;
int id() default -1; // 如果默认值为-1,代表找不到
String[] schools();
}
// 表示我们的注解可以用到什么地方
@Target(value = {ElementType.METHOD,ElementType.TYPE})
// 表示我们的注解在什么地方还有小
@Retention(value = RetentionPolicy.RUNTIME)
// 表示是否将我们的注解生成在JavaDOC中
@Documented
// 子类可以继承父类的注解
@Inherited
@interface MyAnnotation {
// 注解的参数:参数类型+参数名()
String name() default "abc";
int age() default 0;
int id() default -1; // 如果默认值为-1,代表找不到
String[] schools();
}
使用
@MyAnnotation(schools = {"小学","中学"})
public class MetaAnnotaionTest {
// 注解可以显式赋值,如果没有默认值,则必须给注解参数赋值
@MyAnnotation(name = "ccc", schools = {"小学","中学"})
public void test() {
}
}
public class MetaAnnotaionTest {
// 注解可以显式赋值,如果没有默认值,则必须给注解参数赋值
@MyAnnotation(name = "ccc", schools = {"小学","中学"})
public void test() {
}
}
点击右边链接
JDK中的元注解
元注解 用于修饰其他 注解 定义
JDK5.0 提供了4个元注解
@Retention
指定所修饰的 Annotation 的生命周期
包含一个 RetentionPolicy 类型的成员变量
(SOURCE < CLASS < RUNTIME)
(SOURCE < CLASS < RUNTIME)
RetentionPolicy.SOURCE
源文件保留
RetentionPolicy.ClASS
class保留
默认值
RetentionPolicy.RUNTIME
运行时保留
可通过反射获取该注释
图示
使用 @Rentention 时必须为该value 成员变量指定值
@Target
用于指定所修饰的 Annotation 能修饰那些程序元素
value 成员变量
JDK 8.0 有新增,该节下方详解
TYPE_PARAMETER
TYPE_USE
若不加,默认就是都能修饰
@Documented
表示所修饰的注解在被javadoc解析时,保留下来
默认情况下,javadoc是不包括注解的
注意
定义Documented的注解必须设置Retention值为RUNTIME
@Inherited
说明子类可以继承父类中的该注解
如果某个类使用了该注解,则其子类将自动具有该注解
比如
把标有@Inherited注解的自定义的注解标注在类级别上,
子类则可以继承父类级别的注解
子类则可以继承父类级别的注解
利用反射获取注解信息
概念
JDK 5.0 在 java.lang.reflect 包下 新增 AnnotatedElement 接口
AnnotatedElement 接口
代表程序中可以接受注解的程序元素
前提条件:当 Annotation 类型被定义为 运行时
程序可调用 AnnotatedElement 对象的如下方法来访问Annotation 信息
到反射内容时系统讲解
JDK8中注解新特性
可重复注解 @Repeatable
当需要声明同一个 注解多次时,可使用
使用后与之前版本的区别
JDK 8.0 新增
使用方式
在 注解 声明 @Repeatable,成员值 为 注解s.class
注解s 内部属性包含多个 注解
在需要重复使用的注解中 声明, value 为注解集 注解
注解 的 Target 和 Retention 等元注解 和 注解s 相同
实例
声明
注解s
注解
使用
类型注解
概念
JDK 1.8 之后,关于元注解 @Target 的类型参数 ElementType 枚举值 多了两个
TYPE_PARAMETER
TYPE_USE
java 8 之前
注解只能在声明的地方所使用
java 8 开始
注解可以应用在任何地方
新增的 两个 ElementType
TYPE_PARAMETER
表示该注解能写在类型变量的声明语句中(如:范式声明)
TYPE_USE
表示该注解能写在使用类型的任何语句中
使用后
到处都能标记
网络编程
网络编程概述
概念
Java是Internet上地语言,它提供了对网络应用程序地支持
程序员可以开发常见的网络应用程序
Java提供的网络类库,可以实现“无痛”的网络连接
底层细节被隐藏在Java本机安装系统里,由JVM控制
并且在Java实现了一个跨平台的网络库
程序员面对的是一个统一的网络编程环境
计算机网络
网络编程的目的
直接或间接地通过网络协议与其他计算机实现数据交换,进行通讯
网络编程中有两个主要的问题
如何准确地定位网络上一台或多态主机,定位主机上地特定应用
找到主机后如何可靠高效地进行数据传输
网络通信要素概述
要素1:IP和端口号
IP
唯一标识 Internet 上的计算机(通信实体)
在 Java 中使用 InetAddress类 代表 IP
IP分类
IPv4 / IPv6
IPv4
4个字节组成
4个 0-255
以点分十进制表示
如
192.168.0.1
IPv6
128位(16个字节)
8个 无符号整数,每个整数用4个十六进制位表示
以冒号分十六进制表示
如
3ffe:3201:1401:1280:c8ff:fe4d:db39:1984
万维网 / 局域网
万维网
公网地址
局域网
私有地址
范围:192.168.0.0 - 192.168.255.255
特点
不易记忆
域名
域名解析
域名容易记忆,当连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化为IP地址
与主机建立连接
与主机建立连接
本地回路地址
127.0.0.1
对应:localhost
通过网址访问IP的过程
InetAddress
概念
在Java中,一个IP就可以用一个InetAddress对象来表示
实例化方法
getByName(参数)
通过ip/域名参数获取到对应的InetAddress对象
参数
ip地址
域名
实例
编译
getLocalHost()
得到本地对应的 InetAddress对象
实例
编译
常用方法
getHostName()
获取InetAddress对象的域名/主机名
getHostAddress()
获取InetAddress对象的ip地址
端口号
标识正在计算机上运行的进程(程序)
概念
不同的进程有不同的端口号
被规定为一个16位的整数 0-65535
端口分类
公认端口
0-1023
被预先定义的服务通讯端口占用
如
Http
80
Ftp
21
TeInet
23
注册端口
1024-49151
分配给用户进程或应用程序
动态/私有端口
49152-65535
要素2:网络通信协议
概念
一定的规则
即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等指定标准
有两套参考模型
OSI参考模型
模型过于理想化,未能在因特网进行广泛推广
TCP/IP参考模型(或TCP/IP协议)
事实上的国际标准
图示
通信过程
图示
问题
网络协议太复杂
涉及内容很多
指定源地址和目标地址
加密解密
压缩解压缩
差错控制
流量控制
路由控制
解决
分层思想
把复杂的成分分解成一些简单的成分,再将他们复合起来
即同层间可以通信、上一层可以调用下一层,而与下一层不发生关系
各层互不影响,利于系统的开发和拓展
TCP/IP协议簇
传输层协议中有两个非常重要的协议
概念
TCP/IP以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得名,
实际上是一组协议,包括多个具有不同功能且互为关联的协议
实际上是一组协议,包括多个具有不同功能且互为关联的协议
IP(Internet Protocol)协议是网络层的主要协议,支持网间互联的数据通信
TCP/IP协议模型从更实用的角度出发,形成了搞笑的四层体系结构,
即物理链路层、IP层、传输层和应用层
即物理链路层、IP层、传输层和应用层
传输控制协议 TCP(Transmission Control Protocol)
概念
使用TCP协议前,需先建立TCP连接,形成传输数据通道
传输前,采用“三次握手”方式,点对点通信,是可靠的
TCP协议进行通信的两个应用进程
客户端
服务端
在连接中科进行大数据量的传输
传输完毕,需释放已建立的连接,效率低
三次握手
四次挥手
客户端活服务器端均可主动发起挥手动作
在socket编程中,任何一方执行close()操作即可产生挥手操作
详解
TCP网络编程
Socket类
构造函数
new Socket(参数)
参数
实例
客户端
创建Socket对象,指明服务器的ip和端口号
InetAddress cnet = InetAddress.getByName("127.0.0.1");
cs = new Socket(cnet, 12345); //端口号最大值:65535
cs = new Socket(cnet, 12345); //端口号最大值:65535
获取一个输出流,用于输出数据
os = cs.getOutputStream();
写出数据的操作
关闭资源
服务器端
创建服务器端的ServerSocket,指明自己的端口号
ServerSocket ss = new ServerSocket(12345);
调用accept()表示接收来自客户端的socket - 若无接收到,服务器会一直停在这步
Socket socket = ss.accept();
获取socket的输入流
InputStream is = socket.getInputStream();
关闭资源
TCPSocket连接练习
客户端发送消息给服务端,服务端将数据显示在控制台上
TCPSocket连接练习2
客户端发送文件给服务端,服务端将文件保存在本地
TCPSocket连接练习3
客户端发送文件给服务端,服务端将文件保存在本地
并返回“发送成功”给客户端,关闭相应连接(在练习2基础上加了这一步)
用户数据报协议UDP(User Datagram Protocol)
概念
讲数据、源、目的封装成数据包,不需要建立连接
每个数据报的大小限制在64K内
发送不管对方是否准备好,接受方收到也不确认,故是不可靠的
可以广播发送
发送数据结束时无需释放资源,开销小,速度快
UDP网络编程
概念
DatagramSocket 和 DatagramPacket 实现了基于UDP协议网络程序
UDP数据报通过数据报套接字DatagramSocket发送和接收,
系统不保证UDP数据报一定能够安全送到目的地,也不确定什么时候可以抵达
系统不保证UDP数据报一定能够安全送到目的地,也不确定什么时候可以抵达
DatagramPacket对象封装了UDP数据报,
在数据报中包含了 发送端 的IP地址和端口号 和 接收端 的IP地址和端口号
在数据报中包含了 发送端 的IP地址和端口号 和 接收端 的IP地址和端口号
UDP协议中每个数据报都给出了完整的地址信息,
因此无需建立发送方和接收方的连接,如同发快递一样
因此无需建立发送方和接收方的连接,如同发快递一样
DatagramSocket 和 DatagramPacket 类
发送端
创建DatagramSocket
DatagramSocket socket = new DatagramSocket();
创建创建DatagramPacket,指明要发送的地址,数据,数据长度,端口
要发送的地址
InetAddress inet = InetAddress.getLocalHost();
要发送的数据
String str = "数据信息xxxxxxx";
byte[] data = str.getBytes(StandardCharsets.UTF_8);
byte[] data = str.getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(data, data.length, inet, 12345);
使用DatagramSocket发送DatagramPacket
socket.send(packet);
关闭DatagramSocket
socket.close();
接收端
创建DatagramSocket,指明要接收的端口
DatagramSocket socket = new DatagramSocket(12345);
创建DatagramPacket,指明存储数组和有效数据长度
创建接收数据的数组
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
接收数据
socket.receive(packet);
在接收到数据前会一直处于等待阶段
数据操作
关闭DatagramSocket
socket.close();
实例
UDPSocket连接练习1
发送端广播信息,接收端接收并打印
UDPSocket连接练习2
将网上的html文件下载到本地上
URL编程
概念
URL(Uniform Resource Locator):统一资源定位符
表示Internet某一资源的地址
它是一种具体的URI,即URL可以用来标识一个资源,还能指明了如何locate这个资源
通过URL我们可以访问Internet上的各种网络资源
如常见的www,ftp站点
浏览器通过解析给定的URL可以在网络上查找相应的文件或其他资源
URL的基本结构
由5部分组成
<传输协议> :// <主机名> : <端口号> / <文件名> # 片段名 ? 参数列表
如
http :// 192.168.1.100 : 8080 / helloworld/index.jsp / # a ? username=feirouz&password=feirouz
片段名
锚点
比如看小说,直接定位到张杰
参数列表
格式:参数名=参数值&参数名=参数值...
构造器
URL类的构造器都声明抛出非运行时异常,必须对此进行处理
通常try-catch捕获
public URL(String spec)
通过一个标识URL地址的字符串可以构造一个URL对象
如
URL gamelan = new URL("www.itfei.com")
public URL(URL context, String spec)
通过基URL和相对URL构造一个URL对象
如
URL gamelan = new URL(url, "photo.jpg")
public URL(String protocol, String host, String file)
如
URL gamelan = new URL("http", "www.itfei.com", "photo.jpg")
public URL(String protocol, String host, int port, String file)
如
URL gamelan = new URL("http", "www.itfei.com", 80, "photo.jpg")
方法
getProtocol()
协议名
getHost()
主机名
getPort()
端口号
getPath()
文件路径
getFile()
文件名
getQuery()
查询名
实例
URLSocket连接练习1
连接URL,获取详情
URLSocket连接练习2
连接URL,并下载网页到本地
反射
Java反射机制概述
Java Reflection(反射)
Reflect(反射)是被视为 动态语言 的关键
反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息,
并能直接操作任意对象的内部属性及方法
并能直接操作任意对象的内部属性及方法
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象)
这个对象就包含了完整的类的结构信息
我们可以通过这个对象看到类的结构
正常方式
反射方式
动态语言 vs 静态语言
动态语言
运行时代码可以根据某些条件改变自身结构
主要的动态语言
Object-C
C#
JavaScript
PHP
Python
Erlang
静态语言
与动态语言相对应的,运行时结构不可变的语言就是静态语言
主要的静态语言
Java
C
C++
Java不是动态语言,但Java可以称为“准动态语言”
即Java有一定的动态性,可以利用反射机制、字节码操作获得类似动态语言的特性
Java反射机制提供的功能
在运行时判断任何一个对象所属的 类
在运行时构造任意一个类的 对象
在运行时判断任意一个 类所具有的成员变量和方法
在运行时获取 泛型信息
在运行时调用任意一个 对象的成员变量和方法
在运行时处理 注解
生成 动态代理
优点和缺点
优点
可以实现动态创建对象和编译,体现出很大的灵活性
缺点
对性能有影响
使用反射基本上是一种解释操作
我们告诉JVM,我们需玩做什么并且他满足我们的要求
这类操作总是慢于 直接执行相同操作
反射相关的主要API
java.lang.Class
代表一个类
java.lang.reflect.Method
代表类的方法
java.lang.reflect.Field
代表类的成员变量
java.lang.reflect.Constructor
代表类的构造器
.....
实例
不用反射 与 用反射
进行同一操作对比
进行同一操作对比
不使用反射
使用反射
在使用反射后能进行的额外操作
疑问
通过直接new的方式或反射的方式都可以调用公共的结构,开发中到底用哪个?
建议:直接new的方式
什么时候使用反射的方式:当已经运行起来时(反射特征:动态性)
反射机制与面向对象的封装性是不是矛盾的?如何看待两个技术?
不矛盾
然后长篇大论...我不记录了,你懂就行
理解Class类 并 获取Class实例
理解Class 类
在Object类中定义了以下的方法,此方法将被所有子类继承
public final Class getClass()
此类是Java反射的源头
即通过对象反射求出类的名称
注意点
Class 本身也是一个类
Class 对象只能由系统建立对象
一个加载的类 JVM 中只会有一个Class实例
一个Class对象对应的是一个加载到JVM的一个.class文件
每个类的实例都会记得自己是由哪个 Class 实例所生成
通过 Class 可以完整地得到一个类中的所有被加载的结构
Class类 是 Reflection 的根源, 针对任何你想动态加载、运行的类,唯有获得的相应的 Class对象
获得Class类实例的几种方式
方式一:调用运行时类的属性-class
Class c1 = Person.class;
方式二:通过运行时类的对象,调用getClass()
Person p1 = new Person();
Class c2 = p1.getClass();
Class c2 = p1.getClass();
方式三:调用Class的静态方法:forName(String classPath)
这种方式需要处理异常
Class c3 = Class.forName("com.atguigu.java.Person");
(非重点)方式四:内置基本数据类型可以直接用类名.TYPE
Class c4 = Integer.TYPE;
(非重点)方法五:使用类的加载器-ClassLoader
ClassLoader classLoader = ReflectionTest(当前类).class.getClassLoader();
Class c5 = classLoader.loadClass("com.atguigu.java.Persion");
Class c5 = classLoader.loadClass("com.atguigu.java.Persion");
哪些类可以有Class对象
class
外部类
成员(成员内部类,静态内部类)
局部内部类
匿名内部类
interface:接口
[]:数组
enum:枚举
annotation:注解@interface
primitive type:基本数据类型
void
证明实例
Class c1 = Object.class; // 类
Class c2 = Comparable.class; // 接口
Class c3 = String[].class; // 一维数组
Class c4 = int[][].class; // 二维数组
Class c5 = Override.class; // 注解
Class c6 = ElementType.class; // 枚举
Class c7 = Integer.class; // 基本数据类型
Class c8 = void.class; // void
Class c9 = Class.class; // Class
Class c2 = Comparable.class; // 接口
Class c3 = String[].class; // 一维数组
Class c4 = int[][].class; // 二维数组
Class c5 = Override.class; // 注解
Class c6 = ElementType.class; // 枚举
Class c7 = Integer.class; // 基本数据类型
Class c8 = void.class; // void
Class c9 = Class.class; // Class
class java.lang.Object
interface java.lang.Comparable
class [Ljava.lang.String;
class [[I
interface java.lang.Override
class java.lang.annotation.ElementType
class java.lang.Integer
void
class java.lang.Class
interface java.lang.Comparable
class [Ljava.lang.String;
class [[I
interface java.lang.Override
class java.lang.annotation.ElementType
class java.lang.Integer
void
class java.lang.Class
只要元素类型与维度一样,就是同一个Class
Java内存分析
Java内存
堆
存放new的对象和数组
可以被所有的线程共享,不会存放别的对象引用
栈
存放基本变量类型(会包含这个基本类型的具体数值)
引用对象的变量(会存放这个引用在堆里面的具体地址)
方法区
可以被所有得到线程共享
包含了所有的class和static变量
什么时候会发生类的初始化?
类的主动引用(一定会发生类的初始化)
当虚拟机启动,先初始化main方法所在的类
new一个类的对象
调用类的静态成员(除了final常量)和静态方法
使用java.lang.reflect包的方法对类进行反射调用
当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
类的被动引用(不会发生类的初始化)
当访问一个静态域时,只有真正声明这个域的类才会被初始化
如
当通过子类引用父类的静态变量,不会导致子类初始化
通过数组定义类引用
引用常量
实例
new Son()
Main
Father
Son
Father
Son
forName(...)
Main
Father
Son
Father
Son
son.b
Main
Father
2
Father
2
new Son[5]
Main
Son.M
Main
1
1
类的加载与ClassLoader的理解
类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如上三个步骤来对该类进行初始化
类的加载(Load)
将class文件字节码内容加载到内存当中
加载到内存中的类,我们就称为运行时类
此运行时类,就作为Class的一个实例
换句话说,Class的实例就对应着一个运行时类
万事万物皆对象
加载到内存中的类,会缓存一定时间。
在此时间之内,我们可以通过不同的方式来获取此运行时类
在此时间之内,我们可以通过不同的方式来获取此运行时类
并将这些静态数据转换成方法区的运行时数据结构
然后生成一个代表这个类的java.lang.Class对象
作为方法区中类数据的访问入口(即引用地址)
所有需要访问和使用类数据只能通过这个Class对象
类的链接(Link)
将Java类的二进制代码合并到JVM的运行状态之中的过程
验证
确保加载的类信息符合JVM规范
如
以cafe开头
没有安全方面的问题
准备
正式为类变量(static)分配内存 并 设置类变量默认初始值 的阶段
这些内存都将在方法区中进行分配
解析
虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程
类的初始化(Initialize)
类构造器<clinit>()方法 是 由编译器自动收集类中所有类变量的 赋值动作 和 静态代码块中的语句 合并而产生的
类构造器是构造类信息的
不是构造类对象的构造器
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
虚拟机 会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步
实例讲解
类加载器(ClassLoader)
作用
类加载的作用
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构
然后再堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口
类缓存
标准的JavaSE类加载器可以按照要求查找类
但一旦某个类被加载到类加载器中
它将维持加载(缓存)一段时间
JVM垃圾回收机制可以回收这些Class对象
了解CLassLoader
类加载器的作用是用来把类(class)装载进内存的
JVM规范定义了如下类型的类加载器
通过实例来了解
编译运行
拓展
什么是 双亲委派机制 ?
当你创建一个类的时候
他会一级一级往上找
先去用户加载器有没有这个包
再去扩展加载器有没有这个包
再去根加载器有没有这个包
使用ClassLoader加载配置文件
原方式
配置文件位置
通过ClassLoad方式
配置文件位置
调用运行时类的指定结构
属性值
先获取对象属性
获取方式一:getField(属性名)
该种方法不能获取private属性
所以通常不采用该方法
获取方式二:getDeclaredField(属性名)
可获取到该对象的private属性
但若想获取private的值或者设置值,需要设置权限:Field属性.setAccessible(true)
方法
getMethod(参数1, 参数2)
该种方法不能获取private方法
getDeclaredMethod(参数1, 参数2)
可获取到该对象的private方法
但若想调用private的方法,需要设置权限:Method方法.setAccessible(true)
参数1
指明获取的方法名称
参数2
指明获取的方法的形参列表
若没有,可不写
实例
构造器
老师说90%情况使用 上面的方式
通常不用这种方式
通常不用这种方式
getDeclaredConstructor(参数)
获取指定的构造器
参数
指明构造器的参数列表
注意
没有getConstructor()方法,因为构造器不加修饰符
权限也要给够喔
实例
获取运行时类的完整结构(了解)
获取属性结构
getFields()
获取当前运行时类 及 其父类 中声明为public访问权限的属性
实例
编译运行
本类
父类
getDeclaredFields()
获取当前运行时类中声明的所有属性(不包含父类中声明的属性)
实例
编译运行
在获取到属性结构后,可以获得更多细节
获取权限修饰符
getModifiers()
得到是int,什么意思呢?
每个权限修饰都有对应的数
获取数据类型
getType()
获取变量名
getName()
获取方法
getMethods()
获取当前运行时类及其父类中声明为public权限的方法
实例
编译运行
getDeclareMethods()
获取当前运行时类中声明的所有方法(不包含父类中声明的)
实例
获取到方法结构后,可以获得更多细节
获取方法声明的注解
getAnnotations()
注意点:只能获取到运行时的注解
实例
编译
获取修饰符
getModifiers()
实例
获取返回值类型
getRuturnType()
实例
获取方法名
getName()
获取形参列表
getParameterTypes()
说明
通过反射我们还没有办法获取到方法形参的名的
后来版本有提供(但其实我们用的很有限)
实例
子主题
获取抛出的异常
getExceptionTypes()
实例
获取泛型信息
getGenericException()
获取异常信息的泛型
getGenericParameterTypes()
获取参数的泛型
getGenericReturnType()
获取返回的泛型
为了通过反射操作这些类型,
Java新增了如下几种类型来表示不能被归一到Class类中的类型但是又和原始类型齐名的类型
Java新增了如下几种类型来表示不能被归一到Class类中的类型但是又和原始类型齐名的类型
ParameterizedType
表示一种参数化类型
比如
Collection<String>
参数类型集
GenericArrayType
表示一种元素类型是参数化类型或者类型变量的数组类型
参数内泛型集
TypeVariable
是各种类型变量的公共父接口
WildcardType
代表一种通配符类型表达式
实例
本类的方法
Main
编译运行
获取构造器
getConstructors()
获取当前运行时类中声明为public的构造器
实例
编译
getDeclaredConstructors()
获取当前运行时类中声明的所有构造器
实例
编译
获取父类
getSuperclass()
实例
获取带泛型的父类
getGenericSuperclass()
实例
编译
获取泛型父类的泛型类型
paramType.getActualTypeArguments()
实例
编译
获取实现的接口
getInterfaces()
获取父类实现的接口
getSuperclass().getInterfaces()
获取所在的包
getPackage()
获取声明的注解
getAnnotations()
getAnnotation()
练习
了解什么是ORM?
实现代码
点击右边链接
创建运行时类的对象
方式一:new + 构造器
方式二:调用所提供的静态方法
Xxx、Xxxs、XxxFactory、XxxBuilder类中查看是否有静态方法的存在
调用其静态方法,创建Xxx对象
方式三:newInstance()
创建对应的运行时类的对象
内部调用了运行时类的空参构造器
要求
运行时类必须提供空参的构造器
空参的构造器的访问权限得够。(通常-设置为public)
拓展
在 javabean 中要求提供一个public的空参构造器 原因:
便于通过反射,创建运行时类的对象
便于自雷继承此运行时类时,默认调用super()时,保证父类有此构造器
实例
实例应用
方式四:通过构造器创建对象(反射版)
其实就是方式一,不过是反射版的
实例
// 获得class对象
Class c1 = Class.forName("...");
// 构造一个对象
User user = (User)c1.newInstance();
// 获取构造器
Constructor constructor = c1.getDeclaredConstructor(....);
// 通过构造器创建对象
Object o1 = constructor.newInstance(...);
Class c1 = Class.forName("...");
// 构造一个对象
User user = (User)c1.newInstance();
// 获取构造器
Constructor constructor = c1.getDeclaredConstructor(....);
// 通过构造器创建对象
Object o1 = constructor.newInstance(...);
反射的应用:动态代理
原理
使用一个代理将对象包装起来,然后使用该代理对象取代原始对象。
任何对原始对象的调用都要通过代理。
代理对象决定是否以及何时将方法调用转到原始对象上
任何对原始对象的调用都要通过代理。
代理对象决定是否以及何时将方法调用转到原始对象上
之前讲的代理机制,属于静态代理。
其实最好可以通过一个代理类完成全部的代理功能
其实最好可以通过一个代理类完成全部的代理功能
动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象
动态代理使用场合
调试
远程方法调用
动态代理相比于静态代理的优点
静态代理特点:代理类和被代理类在编译期间,就确定下来了
抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,
这样,我们可以更加灵活和统一的处理众多的方法
这样,我们可以更加灵活和统一的处理众多的方法
要想实现动态代理,需要解决的问题
问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象
问题二:当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法a?
使用的方法
代理类中
java.lang.reflect.Proxy.newProxyInstance(ClassLoader, Interfaces, handler)
classLoader
被代理类的ClassLoader
obj.getClass().getClassLoader()
Interfaces
被代理类的接口
obj.getClass().getInterfaces()
handler
实现InvocationHandler接口的类的对象
可以返回代理对象
实现InvocationHandler接口的类中
invoke(proxy, method, args)
proxy
可以使用反射获取代理对象的信息
可以将代理对象返回以进行连续调用
method
即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
args
方法参数
可以在该方法内部调用被代理类方法及相关操作
实例演示
动态代理案例一
AOP与动态代理
AOP(Aspect Orient Programming)
更实用的一种动态代理的机制
Java新特性
Java8其他新特性
概况
Java 8 是 Java语言开发的一个主要版本
发布日期:2014年3月发布
自Java 5以来最具革命性的版本
改善
速度更快
代码更少(Lambda表达式)
强大的 Stream API
便于并行
最大化减少空指针异常:Optional
Nashorn引擎,允许在JVM上运行JS应用
是Java8新一代的js引擎
函数式接口
概念
质包含一个抽象方法的接口,称为函数式接口
可以通过Lambda表达式来创建该接口的对象
若Lambda表达式抛出一个受检异常(即:非运行时异常)
那么改异常需要在目标接口的抽象方法上进行声明
那么改异常需要在目标接口的抽象方法上进行声明
我们可以在一个接口上使用 @FunctionInterface注解
可以检查它是否是一个函数式接口
同时javadoc也会包含一条声明,说明该接口是一个函数式接口
在 java.util.function 包下定义了 Java8 的丰富函数式接口
如何理解函数式接口
随着python、scala等的兴起和新技术挑战,Java不得不做出调整以便支持更加广泛的技术要求
java不但可以支持OOP还可以支持OOF(面向函数编程)
java不但可以支持OOP还可以支持OOF(面向函数编程)
在函数式编程语言中,函数被当做一等公民对单。
在将函数作为一等共鸣的编程语言中,Lambda表达式的类型是函数。
但在Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一种特别的队形类型——函数式接口。
在将函数作为一等共鸣的编程语言中,Lambda表达式的类型是函数。
但在Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一种特别的队形类型——函数式接口。
简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。
这就是Lambda表达式和函数式接口的关系。
也就是说,只要对象是函数式接口的实例,那么该对象就可以用Lambda表达式来写
这就是Lambda表达式和函数式接口的关系。
也就是说,只要对象是函数式接口的实例,那么该对象就可以用Lambda表达式来写
内置四大核心函数式接口
Predicate
Predicate<String> predicate = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.equals("zhangsan");
}
};
@Override
public boolean test(String s) {
return s.equals("zhangsan");
}
};
实例
Predicate
调用方法
main中
若不使用Lambda
其他接口
Lambda表达式
为什么使用 Lambda 表达式
Lambda 是一个 匿名函数,我们可以把 Lambda 表达式理解为是 一段可以传递的代码(将代码像数据一样进行传递)
使用它可以写出更简洁、更灵活的代码
语法
在Java8语言中引入了一种新的语法元素和操作符“->”
->
Lambda操作符 或 箭头操作符
左侧
指定了 Lambda 的形参列表
其实就是接口中抽象方法的形参列表
右侧
指定了 Lambda体
其实就是重写的抽象方法的方法体
需要执行的功能
本质
作为(函数式)接口的实例对象
Lambda 表达式的使用
6种情况
语法格式一:无参,无返回值
Runnable r1 = () -> {
sout("Hello");
};
Runnable r1 = () -> {
sout("Hello");
};
若不使用Lambda
Runnable r1 = new Runnable() {
@Override
public void run() {
sout("Hello");
}
}
@Override
public void run() {
sout("Hello");
}
}
语法格式二:Lambda需要一个参数,无返回值
Consumer<String> con = (String str) -> {
sout(str)
}
Consumer<String> con = (String str) -> {
sout(str)
}
若不使用Lambda
Consumer<String> con = new Consumer(){
@Overide
public void say(String str){
sout(str);
}
}
@Overide
public void say(String str){
sout(str);
}
}
语法格式三:Lambda需要一个参数(数据类型可以省略),无返回值
Consumer<String> con = (str) -> {
sout(str);
}
Consumer<String> con = (str) -> {
sout(str);
}
称为“类型推断”
以前我们就已经接触过了一些
比如
int数组的声明方式
原本
类型推断省略
泛型省略声明
ArrayList<String> list = new ArrayList<>();
若不使用Lambda
Consumer<String> con = new Consumer(){
@Overide
public void say(String str){
sout(str);
}
}
@Overide
public void say(String str){
sout(str);
}
}
语法格式四:Lambda若只需要一个参数,参数的小括号可省略
Consumer<String> con = str -> {
sout(str);
};
Consumer<String> con = str -> {
sout(str);
};
若不使用Lambda
Consumer<String> con = new Consumer(){
@Overide
public void say(String str){
sout(str);
}
}
@Overide
public void say(String str){
sout(str);
}
}
语法格式五:Lambda需要两个或以上的参数,多条执行语句,并且可以有返回值
Comparator<Integer> com = (x, y) -> {
sout("实现函数式接口方法");
return Integer.compare(x, y);
};
Comparator<Integer> com = (x, y) -> {
sout("实现函数式接口方法");
return Integer.compare(x, y);
};
若不使用Lambda
Comparator<Integer> com = new Comparator<Integer> {
@Override
public int compare(Integer x, Integer y) -> {
sout("实现函数式接口方法");
return Integer.compare(x, y);
}
};
@Override
public int compare(Integer x, Integer y) -> {
sout("实现函数式接口方法");
return Integer.compare(x, y);
}
};
语法格式六:当Lambda体只有一条语句时,return与大括号 若有,则可一起省略
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
若不使用Lambda
Comparator<Integer> com = new Comparator<Integer> {
@Override
public int compare(Integer x, Integer y) -> {
return Integer.compare(x, y);
}
};
@Override
public int compare(Integer x, Integer y) -> {
return Integer.compare(x, y);
}
};
实例对比
实例1
未使用
使用
实例2
未使用
使用
方法引用方式(后面会讲)
方法引用 / /
/ 构造器引用 /
/ / 数组引用
/ 构造器引用 /
/ / 数组引用
方法引用
概念
当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用
可以认为是Lambda表达式的一个语法糖
要求
要实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致
格式
使用操作符“::”将类(或对象)与方法名分隔开来
主要三种使用情况(实例)
对象 :: 非静态方法
例一
前提
Consumer
void accept(T t)
PrintStream
void println(T t)
PrintStream ps = System.out
Lambda
Consumer<String> con = str -> ps.println(str);
方法引用
Consumer<String> con = ps :: println;
例二
前提
Supplier
T get()
Employee
String getName()
Employee emp = new Employee(1001, "Tom", 23, 5600);
Lambda
Supplier<String> sup = () -> emp.getName();
方法引用
Supplier<String> sup = emp :: getName;
类 :: 静态方法
例一
前提
Comparator
int compare(T t1, T t2)
Integer
int compare(T t1, T t2)
Lambda
Comparator<Integer> com = (t1, t2) -> Integer.compare(t1, t2);
方法引用
Comparator<Integer> com = Integer :: compare;
例二
前提
Function
R apply(T t)
Math
long round(Double d)
原方法
Function<Double, long> fun = new Function(){
@Override
public Long apply (Double d){
return Math.round(d);
}
}
@Override
public Long apply (Double d){
return Math.round(d);
}
}
Lambda
Function<Double, long> fun = d -> Math.round(d);
方法引用
Function<Double, long> fun = Math :: round;
类 :: 非静态方法
左边函数式接口的其中一个参数 在内部方法中会调用另外一个参数
例一
前提
Comparator
int compare(T t1, T t2)
String
int t1.compareTo(t2)
原方法
Comparator<String> com = new Comparator<>() {
@Override
public int compare(String s1, String s2){
return s1.compareTo(s2);
}
}
@Override
public int compare(String s1, String s2){
return s1.compareTo(s2);
}
}
Lambda
Comparator<String> com = (s1, s2) -> s1.compareTo(s2);
方法引用
Comparator<String> com = String :: compareTo;
例二
前提
BiPredicate
boolean test(T t1, T t2)
String
boolean t1.equals(t2)
原方法
BiPredicate<String> bip = new BiPredicate<>{
@Override
public boolean test(String s1, String s2){
return s1.equals(t2);
}
}
@Override
public boolean test(String s1, String s2){
return s1.equals(t2);
}
}
Lambda
BiPredicate<String> bip = (s1, s2) -> s1.equals(t2);
方法引用
BiPredicate<String> bip = String :: equals;
例三
前提
Function
R apply(T t)
Employee
String getName()
原方法
Function<Employee, String> fun = new Function(){
@Override
public String apply (Employee e){
return e.getName();
}
}
@Override
public String apply (Employee e){
return e.getName();
}
}
Lambda
Function<Employee, String> fun = e -> e.getName();
方法引用
Funcion<Employee, String> fun = Employee :: getName;
构造器引用
和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。
抽象方法的返回值类型即为构造器所属的类的类型
抽象方法的返回值类型即为构造器所属的类的类型
需要new的类 :: new
例一
原方法
Supplier<Employee> sup = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
}
@Override
public Employee get() {
return new Employee();
}
}
Lambda
Supplier<Employee> sup = () -> new Employee();
构造器引用
Supplier<Employee> sup = Employee :: new;
例二
Lambda
Function<Integer, Employee> fun = id -> new Employee(id);
构造器引用
Function<Integer, Employee> fun = Employee :: new;
例三
Lambda
Function<Integer, String, Employee> fun = (id, name) -> new Employee(id, name);
构造器引用
Function<Integer, String, Employee> fun = Employee :: new;
数组引用
大家可以把数组看做一个特殊的类,则写法与构造器引用一致
要new的数组类 :: new
Lambda
Function<Integer, String[]> fun = length -> new String[length];
数组引用
Function<Integer, String[]> fun = String[] :: new;
Stream API
概念
Stream API(java.util.stream)
Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码
Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,
可以执行非常复杂的查找、过滤和映射数据等操作。
类似于使用Sql执行的数据库查询。
可以执行非常复杂的查找、过滤和映射数据等操作。
类似于使用Sql执行的数据库查询。
把真正的函数式编程风格引入到Java中
这是目前为止对Java类库最好的补充。
这是目前为止对Java类库最好的补充。
为什么要用
实际开发中,项目中多数数据源都来自于Mysql、Oracle等,但现在数据源更多了,就需要Java层面去处理
Stream 和 Collection 集合的区别
Collection
一种静态的内存数据结构
面向内存,存储在内存中
Stream
有关计算的
面向CPU,通过CPU实现计算
什么是Stream
是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列
“集合讲的是数据,Stream讲的是计算”
注意
Stream 自己不会存储元素
Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream
Stream 操作是延迟执行的。意味着他们会等到需要结果的时候才执行
Stream的操作 三个步骤
创建 Stream
一个数据源(集合、数组),获取一个流
创建Stream 的四种方式
方式一:通过集合
default Stream<E> stream()
返回一个顺序流
default Stream<E> parallelStream()
返回一个并行流
方式二:通过数组
static <T> Stream<T> stream(T[] array)
返回一个流
重载形式,能够处理对应基本类型的数组
public static IntStream stream(int[] array)
public static LongStream stream(long[] array)
public static DoubleStream stream(double[] array)
实例
方式三:通过Stream的of()
可以调用Stream类静态方法of(),
通过显示值创建一个流。
它可以接收任意输了的参数。
通过显示值创建一个流。
它可以接收任意输了的参数。
public static <T> Stream<T> of(T... values)
返回一个流
实例
方式四:创建无限流
public static <T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
迭代
可能你有点懵,直接看下面实例
UnaryOperator是啥?
上一节的函数式接口,在其他那边
实例
public static <T> Stream <T> generate(Supplier<T> s)
生成
可能你有点懵,直接看下面实例
Supplier是啥
常用的函数式接口,上一节有详细,自己看
实例
并行流
把一个内容分成多个数据块,并用不同线程分别处理每个数据块的流
相较于串行流
可以很大程度提高程序的执行效率
parallel()
串行流
squential()
中间操作
一个中间操作链,对数据源的数据进行处理
Stream 中间操作
1 - 筛选与切片
filter(Predicate p)
接收Lambda,从流中排除某些元素
实例
limit(long maxSize)
截断流,使其元素不超过给定数量
实例
skip(long n)
跳过元素,返回一个扔掉了前n个元素的流
注意
若流中元素不足n个,则返回一个空流
与 limit(n) 互补
实例
distinct()
筛选,通过流所生成的元素 hashCode() 和 equals() 去除重复元素
实例
去重
2 - 映射
map(Function f)
接受一个函数作为参数,该函数会被应用到每个元素上,并将其映射成新的元素
实例
例一
例二
maptoDouble(ToDoubleFunction f)
接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream
mapToInt(ToIntFunction f)
接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream
mapToLong(ToLongFunction f)
接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream
flatMap(Function f)
接受一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流(嵌套的流内部值会被拆出)
和map的区别
类似于list中add和addAll的关系
如该范例
add
对应map
list1.add(list2)
编译
addAll
对应flatMap
list1.addAll(list2)
编译
实例
map
flatMap
对比左边,可以知道,若stream嵌套,则map操作还是嵌套
但flatMap可以拆出来
但flatMap可以拆出来
3 - 排序
sorted()
产生一个新流,其中按自然顺序排序
注意
若是自定义类,使用这种方法的必须实现比较器
实例
sorted(Comparator com)
产生一个新流,其中按比较器顺序排序
实例
终止操作
一旦执行终止操作,就执行中间操作链,并产生结果
之后,不会再被使用
之后,不会再被使用
Stream 终止操作
1 - 匹配与查找
allMatch(Predicate p)
检查是否匹配所有元素
全部匹配才为真
实例
anyMatch(Predicate p)
检查是否至少匹配一个元素
一个匹配就为真
实例
noneMatch(Predicate)
检查是否没有匹配所有元素
全部都不匹配就为真
实例
findFirst()
返回第一个元素
实例
findAny()
返回当前流中的任意元素
实例
count()
返回流中元素总数
实例
max(Comparator c)
返回流中最大值
实例
min(Comparator c)
返回流中最小值
实例
forEach(Consumer c)
内部迭代
特殊说明
使用Collection接口需要用户去做迭代,称为外部迭代
Stream API 使用内部迭代
实例
2 - 归约
reduce(T iden, BinaryOperator b)
可以将流中的元素反复结合起来,得到一个值。返回T
实例
reduce(BinaryOperator b)
可以将流中的元素反复结合起来,得到一个值。返回Optional<T>
实例
或许你想说要是不知道和reduce()里相似的方法怎么办
函数式方法:BinaryOpertor<T>
备注
map 和 reduce 的连接通常称为 map-reduce 模式,因Google用它来进行网络搜索而出名
3 - 收集
collect(Collectors c)
将流转换为其他形式
接收一个Collectors接口的实现,用于给Stream中元素做汇总的方法
Collectors接口
Collectors中方法的实现决定了如何对流执行收集的操作(如收集到List、Set、Map)
主要方法
toList
实例
toSet
实例
其他方法
Optional类
为了在程序中避免出现空指针异常而创建的
概念
Option<T> 类(java.util.Optional)是一个容器类
他可以保存类型T的值,代表这个值存在
或者仅仅保存null,表示这个值不存在
或者仅仅保存null,表示这个值不存在
原来用null表示一个值不存在,现在Optional可以更好的表达这个概念
并且可以避免空指针异常
并且可以避免空指针异常
Optional类的javadoc描述
这是一个可以为null的容器对象
如果值存在则isPresent()会返回true
调用get()会返回该对象
源代码
方法
ofNullable(T t)
实例
orElse(T other)
实例
例一
例二
编译
接口的增强
静态方法
默认方法
新的时间和日期API
其他新特性
重复注解
类型注解
通用目标类型推断
JDK的更新
集合的流式操作
并发
Arrays
Number和Math
IO/NIO的改进
Reflection获取形参名
String:join()
Files
新编译工具:jjs、jdeps
JVM中Metaspace(元空间)取代PermGen(永久代)空间
Java9新特性
Java10新特性
Java11新特性
Java编程基础
Java的基本语法
代码基本格式
注释
标识符
凡是自己起名字的地方都叫标识符
标识符的命名规则
不遵守,编译无法通过
数字、字母、美元符号、下划线
数字不能开头
不可使用关键字和保留字,但可包含关键字和保留字
严格区分大小写,长度无限制
标识符不能包含空格
标识符的命名规范
不遵守,编译可通过
包名
xxxyyyzzz
类名、接口名
XxxYyyZzz
变量名、方法名
xxxYyyZzz
常量名
XXX_YYY_ZZZ
涉及的结构
包名
类名
接口名
变量名
方法名
常量名
关键字
关键字所有字母都为小写
包含
return
范围
方法内使用
作用
结束方法
针对有返回值类型的方法
使用“return 数据”
注意点
return后不可有执行语句
其余(过多)自查
native
....
非关键字
true
false
null
保留字
现阶段java尚未使用,但以后可能会用
包含
goto
const
常量
进制
二进制
以 0b/0B 开头
使用说明
计算机底层的存储方式
所有数字在底层都是以二进制存在
二进制数据的存储方式
所有的数值,不分正负,底层都以补码的方式存储
原码、反码、补码的说明
正数
三码合一
负数
原码
最高位符号位 为1
其余位为 数值转二进制
反码
最高位符号位 不变
对原码取反
补码
其反码 +1
转原码
-1
取反(符号位不变)
例子
例子1
八进制
以 0 开头
十进制
十六进制
以 0x/0X 开头
A-F 不区分大小写
Java中的变量
变量的定义
方式一
数据类型 变量名 = 变量值 ;
方式二
数据类型 变量名 ;
变量名 = 变量值 ;
局部变量定义一定要给初始值,不然编译会报错
拓展
所有变量类型
局部变量
形参
方法局部变量(在方法内定义)
代码块局部变量(在代码快内定义)
成员变量
实例变量
也叫对象变量、类成员变量
类变量(static修饰)
也叫静态变量,是一种比较特殊的实例变量,用static关键字修饰
变量的数据类型
基本数据类型
整数型
byte
1字节
short
2字节
int
4字节
long
8字节
以 'l/L' 结尾
浮点型
float
4字节
以 'f/F' 结尾
double
8字节
字符型
char
2字节 = 1字符
是否使用‘’
使用,‘’内部
一个字符
转义字符
Unicode值
不使用
数字,对应Ascii码
布尔型
boolean
引用数据类型
类
接口
数组
变量的类型转换
基本数据数据类型变量 的 自动类型提升 的 运算规则
byte、short、char -> int -> long -> float -> long
强制转换
自动类型提升的逆运算
强制类型转换可能出现的问题
容量大 -> 容量小
精度损失
变量的作用域 /
/ 按照声明位置分类变量
/ 按照声明位置分类变量
成员变量
实例变量
不以Static修饰
类变量
以Static修饰
局部变量
形参
方法、构造器中定义变量
方法局部变量
在方法内定义
代码块局部变量
在代码块内定义
Java中的运算符
算数运算符
赋值运算符
不会改变变量本身的类型
如
例子1
比较运算符 / 关系运算符
比较运算符
==
可以使用在基本数据类型变量和引用数据类型变量中
比较
基本数据类型变量
比较两个变量保存的数据是否相等。(不一定要类型相同)
注意
基本数据类型中,其余类型不带布尔类型一起玩的
必须保证符号左右两边的变量类型一致
引用数据类型变量
比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体。
关系运算符
逻辑运算符
&
逻辑与
&&
短路与
|
逻辑或
||
短路或
!
逻辑非
^
逻辑异或
拓展
如何交换数据
三种方式
m ^ (m ^ n) = m ^ k = n
n ^ 同一个数两遍 = n
运算实例
位运算符
位运算是直接对整数的二进制进行的运算
<<
左移
在一定范围内,左移n位,在该数基础上 × 2^n
左移1位 — ×2
左移2位 — ×4
左移3位 — ×8
原理
注意
物极必反
整数 共32位
int - 4字节
1字节 - 8位
共 32位
左移过头,就会令符号位为1,得到负数
>>
右移
在一定范围内,右移n位,在该数基础上 ÷ 2^n
左移1位 — ÷2
左移2位 — ÷4
左移3位 — ÷8
同理
注意
右移补的是0还是1
根据符号位
若是负数,左边补1
若是正数,左边补0
>>>
无符号右移
注意
无论是正数还是负数,左边都用0来补
&
与运算
|
或运算
^
异或运算
~
取反运算
运算实例
运算实例1
运算实例2(& | ^)
运算实例3(~)
都是原码
对6取反运算后,得到负数的原码
那么负数的原码如何转为补码
除符号位,取反+1
得-7
三元运算符
运算符的优先级
结构
选择结构语句
if条件语句
switch条件语句
表达式后可跟什么数据类型?
byte
char
short
int
枚举类型变量
JDK5加进来的
String
JDK7加进来的
循环结构语句
while循环语句
do...while循环语句
for循环语句
循环嵌套
跳转语句(break、continue)
foreach循环
概念
Java5.0 提供了 foreach循环 迭代访问 Collection和数组
遍历操作不需获取Collection或数组的长度,无需使用索引访问元素
遍历集合的 底层调用 Iterator 完成操作
使用方式
面试题
方式一会修改数组
方式二遍历是不会导致原数组被修改的
方式二遍历是不会导致原数组被修改的
mind新增时间——刚学完迭代器(从那边复制过来)
嵌套结构
break
结束当前循环
continue
结束当次循环
衡量一个功能代码的优劣
正确性
可读性
健壮性
高效率与低存储
时间复杂度
空间复杂度
数组
数组的特点
数组是有序排列的
数组属于引用数据类型的变量
数组的元素
既可以是基本数据类型
也可以是引用数据类型
引用类型的变量,只能存储null或地址
创建数组的对象会在内存中开辟一块连续的空间
数组的长度一旦确定,就不能修改
数组的分类
按照维数
一维数组
一维数组的声明和初始化
静态初始化
数组的初始化和数组元素的赋值操作 同时进行
int[] ids = new int[]{1, 2, 3, 4,5};
int[] ids = {1, 2, 3, 4,5};
动态初始化
数组的初始化和数组元素的复制操作 分开进行
String[] names = new String[5];
数组初始化完成,长度也就确定了
如何调用数组的指定位置的元素
如何获取数组的长度
数组名.length
如何遍历数组
数组元素的默认初始化值
数组元素是
基本数据类型
整型
默认 0
浮点型
默认 0.0
char型
默认 0 或 '\u0000'
不是 '0'
布尔型
默认 false
引用数据类型
默认 null
非 "null"
数组的内存解析
在内存当中会有几个主要的结构
栈(stack)
存储
局部变量
堆(heap)
存储
new出来的结构
主要指
对象
数组
方法区
常量池
静态域
内存图示
到数组目前只用到栈和堆
数组创建时,内存分析
多维数组
二维数组
二维数组的声明和初始化
静态初始化
数组的初始化和数组元素的赋值操作 同时进行
int[][] ids = new int[][]{{1, 2}, {2, 3}};
int[][] ids = {{1, 2}, {2, 3}};
动态初始化
数组的初始化和数组元素的复制操作 分开进行
String[][] names = new String[5][2];
如何调用数组的指定位置的元素
如何获取数组的长度
如何遍历数组
for(int i = 0; i < arr.length; i++) {
for(int j = 0; j < arr[i].length; j++) {
sout(arr[i][j]);
}
}
for(int j = 0; j < arr[i].length; j++) {
sout(arr[i][j]);
}
}
数组元素的默认初始化值
针对初始化方式一
String[][] names = new String[5][2];
外层元素默认初始化值
地址值
内层元素默认初始化值
与一维数组默认初始化值相同
针对初始化方式二
String[][] names = new String[5][];
外层元素默认初始化值
null
内层元素默认初始化值
报异常(空指针访问异常)
不同情况的 访问二维数组外层与内层的区别
访问二维数组内层和外层的区别
简单的案例测试
数组的内存解析
数组创建操作时,内存分析
图示
......
按照数组元素的类型
基本数据类型元素的数组
引用数据类型元素的数组
数组的常见操作
排序算法
排序的分类
内部排序
内存中完成
外部排序
外部存储器完成
拓展
衡量排序算法的优劣
时间复杂度
空间复杂度
稳定性
算法的5大特征
输入
输出
有穷性
确定性
可行性
十大内部排序算法
选择排序
直接选择排序
堆排序
交换排序
冒泡排序
快速排序
插入排序
直接插入排序
折半插入排序
Shell排序
归并排序
筒式排序
基数排序
Arrays工具类的使用
boolean equals(int[] a, int[] b)
判断两个数组是否相等
String toString(int[] a)
输出数组信息
void fill(int[] a, int val)
将指定值填充到数组之中
void sort(int[] a)
对数组进行排序
int binarySearch(int[] a, int key)
对排序后的数组进行二分法检索指定的值
数组常见的异常
数组角标越界的异常
ArrayIndexOutOfBoundsException
访问的数组下标越界
空指针异常
NullPointerException
数组中无声明,还是null
null还调用方法
其他
main() 方法的使用说明
main() 方法 作为程序的入口
main() 方法 也是一个普通的静态方法
main() 方法 也可以作为我们与控制台交互的方式
之前:使用Scanner
可以这样子
注意点二
简单解析
面向对象
概念
面向对象学习的三大主线
Java类及其成员
类
对一类事物的描述,是抽象的、概念上的定义
对象
是该事物实际存在的个体,也成为实例(instance)
面向对象程序设计的重点
类的设计
设计类就是设计类的成员
2 种权限修饰符都可以用来修饰类
缺省
public
成员
属性、方法、构造器
属性
= 成员变量 = filed = 域、字段
直接定义在类的 {} 内
变量的作用域 /
/ 按照声明位置分类变量
/ 按照声明位置分类变量
成员变量
实例变量
不以Static修饰
在声明属性的时候,可以使用权限修饰符
private
public
缺省
默认的
protected
存在默认初始值
按照不同类型不同默认值
加载到堆空间中
类变量
以Static修饰
局部变量
定义在何处
形参
方法、构造器中定义变量
方法局部变量
在方法内定义
代码块局部变量
在代码块内定义
在声明的时候,不可以使用权限修饰符
不存在默认初始值
必须在调用之前显式赋值
加载到栈空间
属性赋值的先后顺序
执行单次
默认初始化
显式初始化
int 属性 = xxx
构造器中初始化
该步可以反复执行
通过“对象.方法”或“对象.属性”的方式赋值
方法
行为 = 成员方法 = 函数 = method
方法的声明
权限修饰符 返回值类型 方法名(形参列表)
默认权限修饰符是public
方法的使用
可以调用当前类的属性或方法
深入一层
方法的重载
概念
在同一个类种
允许存在一个以上的同名方法
只要它们的参数个数或者参数类型不同即可
“两同一不同”
同一个类、同一方法名
参数列表不同
参数个数不同
参数类型不同
注意
满足重载
与
方法权限修饰符
返回值类型
形参变量名
方法体
特点
与返回值无关
只看参数列表,且参数列表必须不同
在通过对象调用方法时,先如何确定一个指定的方法
方法名
参数列表
可变形参的方法
概念
JavaSE 5.0 中提供了 Varargs(variable number of arguments) 机制
允许直接定义能和 无或单个或多个 实参 相匹配的形参
版本对比
JDK5.0以前
采用数组形参来定义方法,传入多个同类型变量
public static void test(int a, String[] books)
JDK5.0
采用可变个数形参来定义方法,传入多个同一类型变量
public static void test(int a, String... books)
具体使用
可变个数形参格式
数据类型... 变量名
党调用可变个数形参的方法时,传入的个数可以是
0个
1个
2个或更多
可变个数形参的方法与本类中方法名相同
形参不同的方法之间构成重载
形参类型也相同的数组之间不构成重载(一山不能容二虎)
可变个数形参在方法的形参中
必须声明在末尾
最多只能声明一个可变形参
方法参数的值传递机制
形参
方法声明时的参数
实参
方法调用时实际传给形参的参数值
Java实参如何传入方法呢?
Java里方法参数传递方式只有一种
值传递
即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响
形参是基本数据类型
将实参基本数据类型变量的“数据值”传递给形参
形参是引用数据类型
将实参引用数据类型变量的“地址值”传递给形参
递归方法
构造器(constructor)
又名 构造方法
构造器的定义
是否有定义显式的构造器
否
系统默认提供一个空参的构造器
public 类名(){}
是
权限修饰符 类名(形参列表){}
我们写构造器的 权限修饰符 与 类的一致
注意
一旦我们 显式定义过了构造器(重载的定义)
空参的构造器 就一定不能漏了
系统这时候可不会提供默认定义空参构造器了
构造方法的重载
构造器是可以重载的
也就是根据形参不同,可以定义多个,来实现不同的构造方法
一个类中,至少会有一个构造器
一个类中,至少会有一个构造器
构造器的作用
创建类对象
new 构造器
new Person()
初始化对象的属性
注意
要区分构造器和方法的作用
构造器是创建类对象的
而方法是调用对象的方法的
代码块、内部类
4 种权限修饰符都可以用来修饰成员
面向对象的三大特征
封装性
继承性
多态性
(抽象性)
其他关键字
this
super
static
final
abstract
interface
等
面向过程 / 面向对象
面向过程
强调的是功能行为,以函数位最小单位,考虑怎么做
面向对象
强调具备了功能的对象,以类/对象位最小单位,考虑谁来做
类与对象
类的定义
对象的创建与使用
对象.属性
对象.方法
若创建了一个类的多个对象,则每个对象都独立的拥有一套类的属性(非static)
即修改对象属性a,不影响另一对象属性a
操作的三步
创建类、设计类的成员
创建类的实例对象
调用对象的结构(属性、方法)
内存解析
编译完源程序后,生成一个或多个字节码文件(.class)
我门使用JVM中的 类的加载器 和 解释器 对生成的字节码文件进行解释运行
意味
需要将字节码文件对应的类加载到内存中
涉及到内存解析
匿名对象只能使用一次
图示
堆
存放对象实例
new出来的
虚拟机栈
栈
存储局部变量
方法区
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
匿名对象
理解
我们创建的对象,没有显式的赋给一个变量名。即为匿名变量
特征
权限修饰符
体现了 类及其成员(属性、方法、构造器、代码块、内部类)在被调用时 可见性的大小
不同访问修饰符 对应权限2
详情版,不过没必要,就左边这个挺好
不同访问修饰符 对应权限1
拓展知识
JavaBean
指符合以下标准的Java类
类是公共的
有一个无参的公共的构造器
有属性,且有对应的get、set方法
UML类图
OOP特征一:封装性
类的封装
程序最求
高内聚
类的内部数据操作细节自己完成,不允许外部干涉
低耦合
仅对外暴露少量的方法用于使用
隐藏对象内部的复杂性,之对外公开简单的接口
封装性的体现 需要权限修饰符的配合
子主题
this关键字
this 的使用
可以用来修饰、调用 属性、方法、构造器
this 修饰属性和方法
this 可以理解为
当前对象
当前正在创建的对象
描述
在类的方法中
我们可以使用 “this.属性”或“this.方法”
调用当前对象属性或方法
通常情况下
我们都选择省略“this.”
特殊情况下
如果方法的形参和类的属性同名时,我们必须显式的使用“this.变量”的方式
表明此变量是属性
而非形参
在类的构造器中
我们可以使用 “this.属性”或“this.方法”
调用当前正在创建的对象属性或方法
注意点
通常情况下
我们都选择省略“this.”
特殊情况下
如果构造器的形参和类的属性同名时,我们必须显式的使用“this.变量”的方式
表明此变量是属性
而非形参
我们在类的构造器中,可以显式的使用“this(形参列表)”方式
调用本类中指定的其他构造器
注意点
构造器中不能通过“this(形参列表)”方式调用自己
如果一个类有 n 个构造器,则最多有 n - 1 构造器中使用了“this(形参列表)”
规定
“this(形参列表)”必须声明在当前构造器的首行
构造器内部,最多只能声明一个“this(形参列表)”,用来调用其他的构造器
package关键字
使用
为了更改好的实现项目中类的管理,提供包的概念
使用package声明类或接口所属的包,声明在源文件的首行
包,属于标识符,遵循标识符的命名规则、规范(xxxyyyzzz)、“见名知意”
每“.”一次,就代表一层文件目录
补充
同一个包下,不能命名同名的接口、类
不同包下,可以命名同名的接口、类
JDK中主要的包介绍
java.lang
包含一些java语言的核心类
String
Math
Integer
System
Thread
等常用功能
java.net
包含执行与网络相关操作的类和接口
java.io
包含能提供多种输入/输出功能的类
java.util
包含一些实用工具类
定义系统特性
接口的集合框架类
使用与日期日历相关的函数
java.text
包含一些java格式化相关的类
java.sql
包含java进行JDBC数据区变成的相关类/接口
java.awt
包含了构成抽象窗口工具集的多个类
import关键字
注意事项
在源文件中显式的使用import结构导入指定包下的类、接口
import声明在 包的声明和类的声明 之间
如果需要导入多个结构,则并列写出即可
可以使用“xxx.*”的方式,表示可以导入xxx包下的所有结构
如果使用的类或接口是java.lang包下定义的,则可以忽略import结构
如果使用的类或接口是本包下定义的,则可以忽略import结构
如果在源文件中,使用了不同包下的同名的类,则必须至少有一个类需要以全类名的方式显示
使用“xxx.*”方式表明可以调用xxx包下的所有结构。但如果使用的是xxx子包下的结构,则仍需要显示声明
import static:导入指定类或接口中的静态结构:属性或方法
OOP特征二:继承性
继承的概念
继承的好处
减少了代码的冗余,提高了代码的复用性
便于功能的拓展
为之后多态的使用,提供了前提
继承性的格式
class A extends B {}
extends:延展、拓展
A:子类、派生类、subclass
B:父类、超类、基类、superclass
体现
一旦子类A继承父类B以后,子类A中就获取了父类B中声明的所有的属性和方法
特别的,父类中声明为private的属性或方法,子类继承父类以后,仍然认为获取了父类中私有的结构
只有因为封装性的影响,使得子类不能直接调用父类的结构而已
子类继承父类以后,还可以声明自己特有的属性或方法:实现功能的拓展
子类和父类的关系,不同于子类和集合的关系
Java关于继承的规范
Java只支持单继承和多层继承,不允许多重继承
一个子类只能有一个父类
一个父类可以派生出多个子类
子父类是相对概念
子类直接继承的父类,称为:直接父类。
子类间接继承的父类,称为:间接父类
子类继承父类以后,就获取了直接父类以及所有简介父类中声明的属性和方法
与Object类的关系
如果我们没有显式的声明一个类的父类的话,则此类继承于java.lang.Object类
所有的java类(除java.lang.Object类之外)都直接或间接继承于java.lang.Object类
意味着,所有的java类都具有java.lang.Object类声明的功能
方法的重写(override / overwrite)
重写父类方法
概念
子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作
应用
重写以后,当创建子类对象以后,通过子类对象调用字父类中的同名同参数的方法时,实际执行的是子类重写父类的方法
区分方法的重载与重写
两者概念
重载
概念
在同一个类种
允许存在一个以上的同名方法
只要它们的参数个数或者参数类型不同即可
“两同一不同”
同一个类、同一方法名
参数列表不同
参数个数不同
参数类型不同
注意
满足重载
与
方法权限修饰符
返回值类型
形参变量名
方法体
PS
子类也可以定义父类同名不同参来实现重载
重写
概念
子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作
应用
重写以后,当创建子类对象以后,通过子类对象调用字父类中的同名同参数的方法时,实际执行的是子类重写父类的方法
两者在多态中具体情况
在方法调用之前,编译器就已经确定了所要调用的方法。
称为“早绑定”或“静态绑定”
比如多态时的重载
只有等到方法调用的那一刻,编译器(解释运行器)才会确定所要调用的具体方法.
这称为“晚绑定”‘或“动态绑定”
比如多态时的重写
重载不表现多态性
重写表现多态性
重写的规定
方法的声明
权限修饰符 返回值类型 方法名(形参列表) throws 异常类型{
//方法体
}
//方法体
}
约定俗称
子类中的叫重写的方法,父类中的叫被重写的方法
方法的方法名和形参列表
子类重写的 与 父类被重写的相同
子类重写的方法的 权限修饰符 不小于 父类被重写的方法的权限修饰符
特殊情况
子类不能重写父类中声明为private权限的方法
返回值类型
父类被重写的方法的返回值是void,则子类重写的方法的返回值类型只能是void
父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类
本情况基本说明 引用数据类型的情况
基本数据类型情况
父类被重写的方法的返回值类型是基本数据类型(比如:double),则子类重写的方法的返回值类型必须是相同的基本数据类型(比如:double)
子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型
具体详解,在异常章节那边
后期异常方面规定
子类重写的方法 抛出的异常类型 不大于 父类被重写的方法 抛出的异常类型
注意点
子类和父类中同名的方法要么都是声明为非static的(考虑重写),要么都生命为static的(不是重写)
super关键字
理解
父类的
super可以用来调用
属性、方法
我们可以在子类的方法或构造器中。通过使用“super.属性”或“super.方法”的方式,显式的调用父类中声明的属性或方法。
但是,通常情况下,我们习惯省略“super”
特殊情况
当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则必须显式的使用“super.属性”的方式,表明调用的是父类中声明的属性。
当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用“super.方法”的方式,表明调用的时父类中被重写的方法
构造器
我们可以在子类的构造器中显式的使用“super(形参列表)”的方式,调用父类中声明的指定构造器
“super(形参列表)”的使用,必须声明在子类构造器的首行
我们在类的构造器中,针对于“this(形参列表)”或“super(形参列表)”只能二选一,不能同时出现
在构造器的首行,没有显式的声明“this(形参列表)”或“super(形参列表)”,则默认调用的时父类中空参的构造方法:super()
在类的多个构造器中,至少有一个类的构造器中使用了“super(形参列表)”,调用父类的构造器
子类对象实例化的全过程
从结果上来看
(继承性)
子类继承父类以后,就获取了父类中声明的属性或方法
创建子类的对象,在堆空间中,就会加载所有父类中声明的属性
从过程上来看
当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,
知道调用了java.lang.Object类中空参的构造器为止。正因为加载过所有的父类的结构,所以才可以看到内存中有父类的结构,
子类对象才可以考虑进行调用
知道调用了java.lang.Object类中空参的构造器为止。正因为加载过所有的父类的结构,所以才可以看到内存中有父类的结构,
子类对象才可以考虑进行调用
明确
虽然创建子类对象时,调用了父类的构造器,但至始至终就创建过一个对象,即为 new 的子类对象
图例
OOP特征三:多态性
Polymorphism
多态概念
理解多态性
可以理解为一个事物的多种形态
何为多态性
对象的多态性
父类的引用指向子类的对象 /
/ 子类的对象赋给父类的引用
/ 子类的对象赋给父类的引用
多态的使用
虚拟方法调用
当调用子父类同名同参的方法时,实际执行的是子类重写父类的方法
虚拟方法?
Virtual Method Invocation(虚拟方法调用)
按照方法分类
正常方法调用
不使用多态性
声明什么类new什么类
虚拟方法调用
子类中定义了与父类 同名同参的方法,在多态的情况下,将此时父类的方法称为虚拟方法。
父类根据赋给它的不同子类对象,动态调用属于子类的该方法。
这样的方法调用在编译期是无法确定的。
使用前提
类的继承关系
方法的重写
有了对象的多态性以后
编译期
只能调用父类中声明的方法
运行期
实际执行的是子类重写父类的方法
总结
编译
看左边
父类
运行
看右边
子类
对于子类特有的属性和方法
内存中实际上是加载了子类特有的属性和方法的,但是由于变量声明为父类类型。
导致编译时,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。
那么如何才能使用子类特有的属性和方法呢?
使用强制类型转换符
即向下转型
这边还是建议,以后子父类中属性名不要一致
方法可以(重写嘛)
BUT
只适用于方法,不适用于属性
属性
编译 / 运行
看左边
父类
多态是一个运行时行为
例
编译时,e为Person类型,而方法的调用是在运行时确定的,调用的是Student类的方法
—— 动态绑定
对象的类型转换
各种类型转换的称呼和概念
如图
直接(类型)强转
弊端
可能出现ClassCastException的异常。
就是明明是Women类,我专成Men
这时候instanceof关键字诞生了
instanceof关键字
使用
a instanceof A
判断对象a是否是类A的实例。
是
返回true
否
返回false
Tip
a对象/变量 instanceof a对象的类/直接父类或间接父类
使用情景
为了避免在向下转型时
出现ClassCastException异常
最终结果:父类转成子类
不相关类转换
我们会在向下转型之前,先进行instanceof的判断,一旦返回true,就向下转型。如果返回false,不进行向下转型
HOW
肉眼如何判断一个对象是某类实例呢?
Person p = new Man() ;
看右边,p对象就是Man类的一个实例
注意
若你显式instanceof左边和右边,但完全不搭关系,编译直接就报错了,别说运行了
匿名内部类
static关键字
static (静态的)
static可以用来修饰
属性
按照是否使用static修饰,分为
静态属性(类变量)
我们创建了类的多个对象,多个对象共享一个静态变量。
当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,时修改过了的。
其他说明
静态变量随着类的加载而加载
可以通过“类.静态变量”的方式进行调用
静态变量的加载要早于对象的创建
由于类只会加载一次,则静态变量再内存中也只会存在一份:存在方法区的静态域中
调用 类变量 实例变量
类 yes no
对象 yes yes
类 yes no
对象 yes yes
内存解析
栈
局部变量
堆
new出来的结构
对象、数组
方法区
类的加载信息
静态域
常量池
类变量 vs 实例变量 内存解析
图示
非静态属性(实例变量)
我们创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属性。
当修改其中一个对象中的非静态属性时,不会导致其他对象中同样的属性值的修改。
开发中,如何确定是否声明为static
属性是可以被多个对象所共享的,不会随着对象的不同而不同的。
类中的常量也常常声明为static
方法
按照是否使用static修饰,分为
静态方法
其他说明
静态方法随着类的加载而加载
可以通过“类.静态方法”的方式进行调用
调用 静态方法 非静态方法
类 yes no
对象 yes yes
类 yes no
对象 yes yes
静态方法中,只能调用静态的方法或属性
注意点
在静态的方法内,不能使用this关键字、super关键字
关于静态属性和静态方法的使用,大家都从生命周期的角度去理解
非静态方法
其他说明
非静态方法中,既可以调用非静态方法或属性,也可以调用静态的方法或属性
开发中,如何确定是否声明为static
操作静态属性的方法,通常设置为static
工具类中的方法,习惯上声明为static的。
如
Math
Arrays
Collections
单例模式也用到了static
代码块
内部类
当static和final组合时
static final 用来修饰属性
全局常量
代码块
代码块(或初始化块)
作用
用来初始化类、对象
分类
若存在修饰
只能使用static
静态代码块
内部可以有输出语句
随着类的加载而执行,而且只执行一次
作用
初始化类的信息
如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行
静态代码块的执行要优先于非静态代码块的执行
静态代码块内只能调用静态的属性、静态的方法,不能调用非静态的结构
非静态代码块
内部可以有输出语句
随着对象的创建而执行,每创建一个对象,就执行一次非静态代码块
作用
可以在创建对象时,对对象的属性等进行初始化
如果一个类中定义了多个非静态代码块,则按照声明的先后顺序执行
非静态代码块内可以调用静态的属性、静态的方法,或非静态的属性、非静态的方法
对象可以赋值的位置
默认初始化
显式初始化 / 在代码块中赋值
这俩同级,分辨优先度 请看 上下顺序
构造器中初始化
有了对象以后,可以通过“对象.属性”或“对象.方法”的方式,进行赋值
执行先后顺序
① -> ② -> ③ -> ④
构造器、静态代码块、非静态代码块 执行先后顺序
由父及子,静态先行
new √ 非代码块 -> 构造方法
final关键字
final(最终的)
final 可以用来修饰的结构
类
final修饰一个类
此类不能被其他类所继承
如
String类
System类
StringBuffer类
方法
final修饰方法
此方法不可重写
如
Object类中的getClass()
变量
final修饰变量
此时的“变量”就称为一个常量
final修饰属性
那是没有问题的
那考虑一下什么位置可以赋值?
默认初始化
显式初始化 / 在代码块中赋值
构造器中初始化
有了对象以后,可以通过“对象.属性”或“对象.方法”的方式,进行赋值
final修饰局部变量
那是没有问题的
尤其当final修饰形参里也是可以出现的时
表明形参是一个常量
赋值位置
调用方法时传参赋值
方法中
那么只能对他进行调用,不能修改
当static和final组合时
static final 用来修饰属性
全局常量
抽象类与抽象方法
有时将一个父类设计得非常抽象,以至于它没有具体的实例。
这样的类叫做抽象类
这样的类叫做抽象类
abstract 关键字
abstract(抽象的)
可以修饰的结构
类
修饰的类:抽象类
此类不能实例化
抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化的全过程)
开发中,都会提供抽象类的子类,让子类对象实例化,完成相关操作
抽象类使用的前提 -->继承性
方法
修饰的方法:抽象方法
抽象方法只有方法的声明,没有方法体
包含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法的。
子类 是/否 重写父类的所有抽象方法
是
若子类重写了父类中的所有的抽象方法后,此子类可实例化
否
若子类没有重写父类(或间接父类)中所有的抽象方法,则此子类也是一个抽象类,需要使用abstract修饰
注意点
不能用来修饰
属性
构造器
不能用来修饰
私有方法
静态方法
final修饰的方法
final修饰的类
抽象类的匿名子类
非匿名类
非匿名对象
Worker worker = new Worker();
method(worker);
method(worker);
匿名对象
method(new Workers());
匿名类
非匿名对象
Person person = new Person(){
@Override
public void eat(){
System.out.println("吃东西");
}
};
method(person);
@Override
public void eat(){
System.out.println("吃东西");
}
};
method(person);
匿名对象
method(new Person(){
@Override
public void eat(){
System.out.println("吃东西");
}
});
@Override
public void eat(){
System.out.println("吃东西");
}
});
接口
接口和类是两个并列的结构
概念
继承
“是不是”的关系
接口
“能不能”的关系
接口的本质是契约,标准,规范。
可以多重实现接口,但不能多重继承父类。
接口的具体使用:体现了多态性
使用
接口使用 interface 来定义
如何定义接口,定义接口中的成员
JDK7 及以前
只能定义 全局常量 和 抽象方法(无方法体)
全局常量
public static final
但是书写时,可以省略不写
public abstract
但是书写时,可以省略不写
JDK8
除了定义 全局常量 和 抽象方法 之外,还可以定义静态方法、默认方法
静态方法
public static void method1() {
System.out.println("静态方法");
}
System.out.println("静态方法");
}
默认方法
public default void method2() {
System.out.println("默认方法")
}
System.out.println("默认方法")
}
default void method2() {
System.out.println("默认方法")
}
System.out.println("默认方法")
}
这里虽然把public删除了
但是权限依旧是public,请注意
知识点
1
接口中定义的静态方法,只能通过接口来调用
2
通过实现类的对象,可以调用接口中的默认方法
如果实现类重写了接口中的默认方法,调用时,仍是调用重写以后的方法
3
如果子类(或实现类)继承的父类和实现的接口中声明了同名同参的默认方法,
那么子类在没有重写此类方法的情况下,默认调用的是父类中的同名同参数的方法
那么子类在没有重写此类方法的情况下,默认调用的是父类中的同名同参数的方法
类优先原则
4
如果实现类实现了多个接口(无 继承 含同名方法的父类 情况),而这多个接口中定义了同名同参数的默认方法,
那么在实现类没有重写此方法的情况下,报错。--> 接口冲突
那么在实现类没有重写此方法的情况下,报错。--> 接口冲突
解决方式:在实现类中重写此方法
5
如何在子类(或实现类)的方法中调用父类、接口中被重写的方法
调用父类的方法
super.方法名
调用接口中的默认方法
接口名.super.方法名
JDK9
可以声明 私有方法
接口中不能定义构造器!
意味着接口不可以实例化
Java开发中,接口通过让类去实现(implements)的方式来使用
如果实现类覆盖了接口中的所有抽象方法,则此实现类就可以实例化
如果实现类没有覆盖接口中的所有抽象方法,则此实现类仍为一个抽象类
Java 类可以实现多个接口
弥补了 Java 单继承的局限性
格式
class A extends B implements C, D, E
内部类
类的内部成员
Java 中允许将一个类A声明在另一个类B中,则类A就是内部类,类B称为外部类
内部类的分类
成员内部类
静态
非静态
能干嘛?
一方面,作为外部类的成员
调用外部类的结构
如
在内部类中调用外部类的方法
class Bird {
public void sing() {
eat(); //外部类的方法
外部类名称.this.eat(); //实际上前面省略了 外部类名称.this.
}
}
public void sing() {
eat(); //外部类的方法
外部类名称.this.eat(); //实际上前面省略了 外部类名称.this.
}
}
可以被static修饰
可以被4种不同权限修饰
另一方面,作为一个类
类内可以定义属性、方法、构造器等
可以被final修饰,表示此类不能被继承
u1s1,final修饰后的类本来就不能被继承
可以被abstract修饰
关注两个问题
如何实例化成员内部类的对象?
main {
// 创建 内部类的实例(静态的成员内部类)
外部类.内部类 dog = new 外部类.内部类();
// 创建 内部类的实例(非静态的成员内部类)
外部类 p = new 外部类();
外部类.内部类 bird = p.new 内部类();
}
// 创建 内部类的实例(静态的成员内部类)
外部类.内部类 dog = new 外部类.内部类();
// 创建 内部类的实例(非静态的成员内部类)
外部类 p = new 外部类();
外部类.内部类 bird = p.new 内部类();
}
注意
创建 非静态的 内部类实例时
外部类.内部类 bird = new 外部类.内部类(); 是错的,因为非静态,你不能这么调用
如何在成员内部类中区分调用外部类的结构?
一个内部类方法中,形参为name
sout(name); //方法的形参
sout(this.name); //内部类的属性
sout(Person.this.name); //外部类的属性
局部内部类
方法内
代码块内
构造器内
关注一个问题
开发中局部内部类的使用?
例子
比如一个方法,要求你返回指定类对象,
这时候就可以在方法内定义内部类
这时候就可以在方法内定义内部类
在编译后,都会生成字节码文件
成员内部类
外部类$内部类名.class
局部内部类
外部类$数字+内部类名.class
由于局部内部类可以声明在 方法体、代码块、构造器种
所以表明同一个类名字可能多次出现
这里的 数字 就是用来区别 同名类的
小的知识点
public void method() {
// 局部变量
int num = 10;
class AA {
public void show() {
// num = 20;
System.out.println(num)
}
}
}
// 局部变量
int num = 10;
class AA {
public void show() {
// num = 20;
System.out.println(num)
}
}
}
在局部内部类的方法中(比如:show如果调用局部内部类所声明的方法(method)中的局部变量(num)的话),
要求此局部变量声明为 final 的
要求此局部变量声明为 final 的
观察以上代码
JDK 7 及以前
这样子会编译报错
因为局部内部类调用 局部变量的话,要求局部变量显式声明为 final
JDK 8 及以后
这样子不会编译报错
可以省略 final 的声明
但是其实 这里的 num 被定义成了 final,是不能被修改的
为啥要这样?
个人理解
这时候已经到你内部类了,然后你还调用外部的变量,其实就相当于调用一个外部传进来的副本,你有必要修改他吗?
修改它也改不了之前外部类中的变量情况,所以干脆就 final,不可修改,我们直接调用已经确定的值。
设计模式
什么是设计模式?
设计模式是在大量的实践中总结和理论化之后优选的代码结构、编码风格、以及解决问题的思考方式。
“套路”
23种经典设计模式
创建型模式(Creational Pattern)
单例模式(Singleton)
采取一定的方法保证整个的软件系统中,对于某个类 只能存在一个对象实例
优点
减少系统开销
如何创建单例模式?
饿汉式
私有化类的构造器
内部创建类的对象
提供公共的静态方法,返回类的对象
由于静态方法调用,则此对象也必须声明为静态的
懒汉式
私有化类的构造器
声明当前类的对象,没有初始化
提供公共的静态方法,返回类的对象
由于静态方法调用,则此对象也必须声明为静态的
饿汉式/懒汉式 优缺点
饿汉式
优点
饿汉式是线程安全的
缺点
对象加载时间会被延长
懒汉式
优点
会延迟对象的创建时间
缺点
若不使用多线程,则会有线程不安全的问题
使用单例模式的例子?
如
Runtime类
单例模式-饿汉式
数据库连接池
网站的计数器
项目中,读取配置的类
工厂设计模式<ALL>
分类
无工厂模式
这不是设计模式,就是简单的new罢了
简单工厂模式
其实可以看作 包含 在 工厂方法模式 里的
广义上
工厂方法模式(FactoryMethod) / 工厂模式(Factory)
包括了 简单工厂 和 工厂方法
工厂方法模式(FactoryMethod) / 工厂模式(Factory)
Tip
接口的应用
抽象工厂模式
建造者模式
原型模式
结构型模式(Structural Pattern)
适配器模式
装饰器模式
代理模式(Proxy)
意图
为其他对象提供一种代理以控制对这个对象的访问。
主要解决的问题
在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。
在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),
直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),
直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
应用场景
安全代理
屏蔽对真实角色的直接访问
远程代理
通过代理类处理远程方法调用(RMI)
延迟加载
先加载轻量级的代理对象,真正需要再加载真是对象
如
开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有100MB,在打开文件时,
不可能将所有的图片都显示出来,这样就可以使用代理模式,当需要查看图片时,用proxy来进行大图片的打开
不可能将所有的图片都显示出来,这样就可以使用代理模式,当需要查看图片时,用proxy来进行大图片的打开
分类
静态代理(静态定义代理类)
动态代理(动态生成代理类)
√JDK自带的动态代理,需要反射等知识
优缺点
优点
1、职责清晰。 2、高扩展性。 3、智能化。
缺点
1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
Tip
接口的应用
外观模式
桥接模式
组合模式
享元模式
行为型模式(Behavioral Pattern)
策略模式
模板方法模式(TemplateMethod) / 模板模式(Template)
解决的问题
当功能内部一部分实现是确定的,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。
换句话说
在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。
但是某些部分易变,易变部分可以抽象出来,供不同子类实现。
这就是一种模板模式。
Tip
抽象关键字的使用
观察者模式
迭代子模式
责任链模式
命令模式
备忘录模式
状态模式
访问者模式
中介者模式
解释器模式
垃圾回收
异常处理
异常概述与异常体系结构
异常概述
在Java语言中,讲程序执行中发生的不正常情况称为“异常”
(开发过程中的语法错误和逻辑错误不是异常)
异常分类
Error
Java虚拟机无法解决的严重问题。
如
JVM系统内部错误
资源耗尽等严重情况
一般不编写针对性的代码进行处理。
Exception
其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理
如
空指针访问
试图读取不存在的文件
网络连接中断
数组角标越界
解决方式
一
终止程序运行
二
捕获错误,提前考虑到错误的检测、错误消息的提示,以及错误的处理
异常体系结构
java.lang.Throwable
java.lang.Error
一般不编写针对性的代码进行处理
java.lang.Exception
可以进行异常的处理
编译时异常(checked)
IOException
FileNotFoundException
ClassNotFoundException
....
运行时异常(unchecked)
NullPointerException
ClassCastException
NumberFormatException
....
图示
蓝色:非受检(unchecked) - 运行时异常
红色:受检(checked) - 编译时异常
图示
常见异常
抓抛模型
过程一
“抛”
程序在正常的执行过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象。
并将此对象抛出。
并将此对象抛出。
一旦抛出对象以后,其后的代码就不再执行。
关于异常对象的产生
系统自动生成的异常对象
系统自动生成的异常对象
手动的生成一个异常对象,并抛出(throw)
过程二
“抓”
可以理解为异常的处理方式
try-catch-finally
throws
异常处理
Java采用的异常处理机制,是将异常处理的程序代码集中在一起,
与正常的程序代码分开,使得程序简洁、优雅,并易于维护
与正常的程序代码分开,使得程序简洁、优雅,并易于维护
try-catch-finally
使用
try{
// 可能出现的异常的代码
}catch(异常类型1 变量名1){
// 处理异常类型1的方式
}catch(异常类型2 变量名2){
// 处理异常类型2的方式
}
.....
finally{
// 一定会执行的代码
}
// 可能出现的异常的代码
}catch(异常类型1 变量名1){
// 处理异常类型1的方式
}catch(异常类型2 变量名2){
// 处理异常类型2的方式
}
.....
finally{
// 一定会执行的代码
}
说明
使用 try 将可能出现异常代码包装起来,在执行过程中。
一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配
一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配
一旦 try 中的异常对象匹配到某个 catch 时,就进入 catch 中进行异常处理。
一旦处理完成,就跳出当前的 try-catch 结构(在没有写 finlly 的情况)。继续执行其后的代码
一旦处理完成,就跳出当前的 try-catch 结构(在没有写 finlly 的情况)。继续执行其后的代码
catch 中的异常类形参 的顺序
若无子父类关系
谁在上谁在下无所谓
若满足子父类关系
则要求子类一定声明在父类上面
否则报错
常用的异常对象处理方式
String getMessage()
获得错误信息
printStackTrace()
在控制台打印出异常种类,错误信息和出错位置等
在 try 结构中声明的变量,在出了 try 结构以后,就不能被调用
finally 是可选的
finally 中声明的是一定会执行的代码。
即使
catch 中有return 语句
try中有 return 语句
catch 中又出现异常了
...情况
finally 内经常声明什么?
像数据库连接、输入输出流、网络编程Socket 等资源,JVM是不能自动回收的,
我们需要手动的进行资源的释放。
我们需要手动的进行资源的释放。
此时的资源释放,就需要声明在finally中
try-catch-finally 结构可以嵌套
体会
使用 try-catch-finally 处理编译时异常,是得程序在编译时就不再报错,但是运行时仍可能报错。
相当于我们使用 try-catch-finally 将一个编译时可能出现的异常,延迟到运行时出现。
相当于我们使用 try-catch-finally 将一个编译时可能出现的异常,延迟到运行时出现。
开发中,由于运行时异常比较常见,所以我们通常不针对运行时异常编写 try-catch-finally 了。
针对于编译时异常,我们说一定要考虑异常的处理。
针对于编译时异常,我们说一定要考虑异常的处理。
补充点
方法重写的规则之一
子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型
子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型
throws + 异常类型
说明
“throws + 异常类型”写在方法的声明处。
指明此方法执行时,可能会抛出的异常类型
一旦方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后异常类型时,就会被抛出。
异常代码后续的代码,不再执行!
体会
try-catch-finally
真正的将异常给处理掉了
throws
只是将异常抛给了方法的调用者。
并没有真正将异常处理掉。
注意
在子父类重写时
子类重写的方法 抛出的异常类型 不大于 父类被重写的方法 抛出的异常类型
开发中 如何选择使用 try-catch-finally 还是使用 throws?
如果 父类中被重写的方法 没有throws方式处理异常,则 子类重写的方法 也不能使用throws。
意味着如果 子类重写的方法 中有异常,必须使用 try-catch-finally 方式处理。
执行的方法a 中,先后调用了另外的几个方法,这几个方法是递进关系执行的。
我们建议这几个方法使用 throws 的方式处理。
而执行的方法a可以考虑使用 try-catch-finally 的方式进行处理
手动抛出异常:throw
使用
{ // 方法体
throw new Exception("您输入的数据非法");
}
throw new Exception("您输入的数据非法");
}
注意
手动抛出运行时异常,是不会报错的
用户自定义异常类
如何自定义?
继承于现有的异常结构
RuntimeException
Exception
提供全局常量
serialVersionUID
提供重载的构造器
如
public class 异常类名 extends 异常 {
static final long serialVersionUID = 编码;
public EcDef() {}
// 非必须
public EcDef(String msg) {
super(msg);
}
}
static final long serialVersionUID = 编码;
public EcDef() {}
// 非必须
public EcDef(String msg) {
super(msg);
}
}
Java API
Object类
Object类是所有Java类的根父类
如果在类的声明中未使用extends关键字指明其父类,则默认父类为java.lang.Object类
Object类中的功能(属性、方法)就具有通用性
属性
无
方法
clone
创建并返回一个对象的拷贝
equals(Object)
对象比较
是一个方法,而非运算符
只能适用于引用数据类型
Object类中equals()的定义
public boolean equals(Object obj) {
return (this == obj);
}
return (this == obj);
}
说明
Object类中定义的 equals() 和 == 的作用是相同的。
像String、Date、File、包装类等都重写了Object类中的equals()方法
重写以后,比较的不是两个引用的地址是否相同,二十比较两个对象的 “实体内容” 是否相同。
通常情况下
我们自定义的类如果使用equals()的话,也通常时比较两个对象的“实体内容”是否相同
那么我们就需要对Object类中的equals()进行重写
重写的原则
比较两个对象的实体内容是否相同
finalize
当对象被回收之前,会调用当前对象的finalize方法
不需要主动调用,一般交由垃圾回收器来自动调当前对象的finalize方法
getClass
后面反射常用
hashCode
返回当前对象的哈希值
toString()
当我们输出一个对象的引用时,实际上就是调用当前对象的toString()
Object类中的toString()的定义
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
像String、Date、File、包装类等都重写了Object类中的toString()方法。
使得在调用对象的toString()时,返回 “实体内容” 信息
自定义类也可以重写toString()方法,当调用此方法时,返回对象的 “实体内容”
notify()
notifyAll()
wait()
wait(long timeout)
wait(long timeout,int nanos)
Object类只声明了一个空参构造器
包装类
为什么要有包装类
为了使基本数据类型具有类的特征,引入包装类
针对八种基本数据类型定义相应的引用类型——包装类(封装类)(Wrapper)
基本数据类型、包装类和String类之间的转化
String类 转 基本数据类型、包装类
boolean b1 = Boolean.parseBoolean(str2);
int num2 = Integer.parseInt(str1);
基本数据类型、包装类 转 String类
String str2 = String.valueOf(11.2f);
注意
转换时,可能会报NumberFormatException
字符串相关的类
String类及常用方法
概念
代表字符串
Java程序中所有字符串面值(如“abc”)都作此类的实例实现
源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
......
}
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
......
}
说明
String声明为 final 的,不可继承的
String实现了 Serializable接口:表示字符串是可以序列化的
String实现了 Comparable接口:表示String可以比较大小
String内部定义了 final char[] value 用于存储符串数据
String:代表不可变的字符序列。(简称:不可变性)
体现
当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池种
字符串常量池中是不会存储相同内容的字符串的
内存中的表现
String对象的常见创建方式
方式一: 通过字面量定义的方式
方式二: 通过new + 构造器的方式
代码
String str = "hello";
// 本质上this.value = new char[0];
String s1 = new String();
// this.value = original.value;
String s2 = new String(String original);
// this.value = Arrays.copyOf(value, value.length);
String s3 = new String(char[] a);
String s4 = new String(char[] a, int startIndex, int count);
// 本质上this.value = new char[0];
String s1 = new String();
// this.value = original.value;
String s2 = new String(String original);
// this.value = Arrays.copyOf(value, value.length);
String s3 = new String(char[] a);
String s4 = new String(char[] a, int startIndex, int count);
面试题
String str1 = "abc" 和 String str2 = new String("abc") 的区别
内存中的表现
带地址版
String str2 = new String("abc") 创建对象,在内存中创建了几个对象?
两个
一个是堆空间的new结构
另一个是char[] 对应的常量池中的数据:”abc“
为啥不是3个?
str2 是变量,不是对象
JVM里涉及 字符串的内存结构
<- JDK1.6
永久代(方法区)存放了类信息、常量(运行时常量池、字符串常量池)、静态变量以及即时编译后的代码
JDK 1.7
字符串常量池从方法区搬移到堆中
JDK 1.8
元空间 metaspace 取代了永久代
更多的话在 《JVM解析》那边
String常用方法
int length()
返回字符串的长度
return value.length
char charAt(int index)
返回某索引处的字符
return value[index]
boolean isEmpty()
判断是否是空字符串
return value.length == 0
String toLowerCase()
使用默认语言环境,将 String 中的所有字符转换位小写
String toUpperCase()
使用默认语言环境,将 String 中的所有字符转换位大写
String trim()
返回字符串的副本,忽略前导空白和尾部空白
boolean equals(Object obj)
比较字符串的内容是否相同
boolean equalsIgnoreCase(String anotherString)
比较字符串的内容是否相同
与 equals() 类似,但忽略大小写
String concat(String str)
将指定字符串连接到此字符串的结尾
等价于用 ”+“
int compareTo()
比较两个字符串的大小
返回
正数
调用方法的字符串 > 被比较字符串
负数
调用方法的字符串 < 被比较字符串
例如
比较"a"和"c"
会返回 -2(-2 = 97-99)
String substring(int beginIndex)
返回一个新的字符串,它是此字符串 范围[beginIndex,+∞) 的一个子字符串
String substring(int beginIndex, int endIndex)
返回一个新的字符串,它是此字符串 范围[beginIndex, endIndex) 的一个子字符串
左闭右开
boolean endsWith(String suffix)
测试此字符串是否以指定的后缀结束
返回值
满足
true
不满足
false
boolean startsWith(String prefix)
测试此字符串是否以指定的前缀开始
返回值
满足
true
不满足
false
boolean startsWith(String prefix, String suffix)
测试此字符串 从指定索引开始的子字符串 是否以指定的前缀开始
返回值
满足
true
不满足
false
boolean contains(CharSequence s)
当且仅当此字符串包含指定的char值序列时,返回true
int indexOf(String str)
返回指定子字符串在此字符串中第一次出现的索引
未找到返回 -1
int indexOf(String str, int formIndex)
未找到返回 -1
返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
int lastIndex(String str)
未找到返回 -1
返回指定子字符串在此字符串中(从右边开始)第一次出现的索引
int lastIndex(String str, int fromIndex)
返回指定子字符串在此字符串中(从右边开始)第一次出现处的索引,从指定的索引开始反向搜索
未找到返回 -1
String replace(char oldChar, char newChar)
返回一个新的字符串
通过用 newChar 替换此字符串中所有的 oldChar 得到的
String replace(CharSequence target, CharSequence replacement)
返回一个新的字符串
通过用 字面值 替换此字符串中所有的 匹配字面值目标序列的子字符串 得到的
String replaceAll(String regex, String replacement)
返回一个新的字符串
使用给定的 replacement 替换此字符串中 所有 匹配给定的正则表达式的子字符串
String replaceFirst(String regex, String replacement)
返回一个新的字符串
使用给定的 replacement 替换此字符串中 第一个 匹配给定的正则表达式的子字符串
boolean matches(String regex)
告知此字符串是否匹配给定的正则表达式
String[] split(String regex)
根据 给定正则表达式 的匹配拆分此字符串
返回字符串数组
String[] split(String regex, int limit)
根据 给定正则表达式 的匹配拆分此字符串
最多不超过 limit个,如果超过了,剩下的全部放到最后一个元素中
返回字符串数组
类型转换
与 基本数据类型、包装类 之间的转换
String --> 基本数据类型、包装类
调用包装类的静态方法
Xxx.parseXxx(str)
基本数据类型、包装类 --> String
调用String重载的valueOf(xxx)
String.valueOf(xxx)
使用变量与空字符串拼接方式
xxx + ""
与 字符串数组 之间的转换
字符串数组 -> 字符串
使用 String类的构造器
String(char[])
用全部字符创建字符串对象
String(char[], int offset, int length)
用部分字符创建字符串对象
字符串 -> 字符串数组
调用 String类的 toCharArray()
public char[] toCharArray()
将字符串中的全部字符存放在一个字符数组中
调用 String类的 getChars()
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
rcBegin -- 字符串中要复制的第一个字符的索引。
srcEnd -- 字符串中要复制的最后一个字符之后的索引。
dst -- 目标数组。
dstBegin -- 目标数组中的起始偏移量。
将指定索引范围内的字符串存放到数组中
与 字节数组 之间的转换
字节数组 -> 字符串(解码)
解码
编码的逆过程
看不懂的二进制数据 ---> 看得懂
解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码
调用 String 的构造器
String(byte[])
通过使用平台的默认字符集编码,将指定byte数组构造一个新的String
String(byte[], int offset, int length)
通过使用平台的默认字符集编码,将指定的byte数组的一部分(即从数组开始位置offset取length个字节)构造一个新的String
String(byte[], String charsetName)
通过使用指定字符集编码,将指定的byte数组的一部分(即从数组开始位置offset取length个字节)构造一个新的String
字符串 -> 字节数组(编码)
编码
看得懂 ---> 看不懂的二进制数据
调用 String 的 getBytes()
public byte[] getBytes()
使用平台默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte数组中
public byte[] getBytes(String charsetName)
使用指定的字符集将此 String 编码到 byte 序列,并将结果存储到新的 byte数组
例子
图示(这里默认字符集是 utf-8)
编译
可以看到
utf-8
1个中文占3个字节
gbk
1个中文占2个字节
StringBuffer
概念
代表可变的字符序列
可以对字符串内容进行增删,此时不会产生新的对象
声明版本
JDK1.0
所有方法 都带 synchronized
线程安全的
效率低
其他说明
很多方法与String相同
作为参数传递时,方法内部可以改变值
类层级关系
源码
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
....
}
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
....
}
value没有final声明,value可以不断扩容
count记录有效字符的个数
StringBuilder
概念
代表可变的字符序列
可以对字符串内容进行增删,此时不会产生新的对象
和StringBuffer 非常类似,而且提供相关功能的方法也一样
声明版本
JDK5.0
所有方法 都不带 synchronized
线程不安全的
效率高
类层级关系
源码
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
....
}
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
....
}
value没有final声明,value可以不断扩容
count记录有效字符的个数
日期时间API
JDK8之前
java.lang.System
概念
System类提供的public static long currentTimeMillis() 用来返回当前与1970年1月1日0时0分0秒之间以毫秒为单位的时间差
计算世界时间的主要标准有
UTC
GMT
CST
此方法适用于计算时间差(时间戳)
方法
public static long currentTimeMillis()
返回当前与1970年1月1日0时0分0秒之间以毫秒为单位的时间差
这个时间差 称为 时间戳
实例
Date类
java.util.Date
表示特定的瞬间,精确到毫秒
构造器
Date()
创建一个对应当前时间的Date对象
Date(long date)
创建指定毫秒数的Date对象
常用方法
getTime()
获取自1970年1月1日 00:00:00 GMT 以来此Date对象表示的毫秒数(时间戳)
实例
toString()
把此Date对象转换为 以下形式的 String:
dow mon dd hh:mm:ss zzz yyyy
dow mon dd hh:mm:ss zzz yyyy
dow
星期几
zzz
时间标准
实例
显示当前的年、月、日、时、分、秒
其他很多方法都过时了
java.sql.Date
继承 java.util.Date
构造器
和java.util.Date一致
常用方法
和java.util.Date一致
SimpleDateFormat类
概述
Date类的API不易于国际化,大部分被废弃了
java.text.SimpleDateFormat类是一个不与语言环境有关的方式来格式化和解析日期的具体类
它允许进行
格式化
日期 -> 文本
解析
文本 -> 日期
使用
构造器
SimpleDateFormat()
默认的模式和语言环境创建对象
SimpleDateFormat(String pattern)
该构造方法可以用 参数pattern 指定的格式创建一个对象,该对象调用
格式范例
实例
格式化
概述
日期 -> 文本
方法
String format(Date date)
方法格式化时间对象date
实例
编译
解析
文本 -> 日期
方法
Date parse(String source)
从给定字符串的开始解析文本,以生成一个日期
实例
编译
注意点
要求字符串必须是符合SumpleDateFormat识别的格式(通过构造器参数体现)
否则,抛异常
ParseException
Calendar类
概念
Calendar是一个抽象类,主要用于完成日期字段之间相互操作的功能
注意
获取月份时
1月是0,以此类推,12月是11
获取星期时
周日是1,以此类推,周六是7
JDK1.1 出现
获取Calendar实例的方法
Calendar.getInstance()
调用子类 GregorianCalendar 的构造器
常用方法
get()
能get很多东西,可以看API
比如当前日期是一年第几天、一个月第几天、一个星期第几天...
set()
也能设置当前是第几天
比如当前日期是一年第几天、一个月第几天、一个星期第几天...
这里体现了Calendar类的可变性
add()
也能让当前日期增加多少天
实例
编译(增加3天,从22变成25)
若想减去多少天
值改为负数即可
实例
编译(减少3天,从22变成19)
与Date之间的转换
getTime()
日历类 -> Date
setTime()
Date -> 日历类
关系图
关系图
JDK8之后
新日期时间API
出现的背景
一开始为时间错,但是我们希望时间与昼夜、四季相关
JDK1.0 包含了一个java.util.Date类,
但它的大多数方法在 JDK1.1 引入Calendar类后被弃用了
但它的大多数方法在 JDK1.1 引入Calendar类后被弃用了
而Calendar并没有比Date好多少
Calendar的问题
可变性
日期应不可变,但Calendar可变
偏移性
年份从1900开始,月份从0开始
源码
Date设置源码
格式化
格式化只对Date有用
不是线程安全的,不能处理闰秒...
对日期和时间的操作一直是Java程序员最痛苦的地方之一
概念
第三次引入的API是成功的
并且 Java8 引入的 java.time API 已经纠正了过去的缺陷
将来很长一段时间内都会为我们服务
Java8 吸收了 Joda-Time 的精华,以一个新的开始为Java创建优秀的API
历史悠久的 Date类 新增了 toInstant() 方法
用于把Date转换成新的表示形式
java.time
包含值对象的基础包
包含类
LocalDate
本地日期
代表IOS格式(yyyy-MM-dd)的日期
LocalTime
本地时间
表示一个时间,非日期
LocalDateTime
本地日期时间
表示时间+日期,这是最常用的类之一
Instant
瞬时(Instant)
概念
时间线上的一个瞬时点。(时间戳)
java.time 包 通过值类型 Instant 提供机器视图,不提供处理人类意义上的时间单位
Instant表示时间线上的一点,而不需要任何上下文信息,例如时区。
它只是简单的 表示自1970年1月1日0时0分0秒(UTC)开始的毫秒数。
由于java.time包基于纳秒计算
所以Instant的精度可以达到纳秒级
1秒 = 1000毫秒 = 1000 000微秒 = 1000 000 000纳秒
类似与 Java.util.Date类
常见方法
图示
now()
获取本初子午线对应的标准时间
若想获取东八区(我们所处)的时间,需要自行添加偏移量
atOffset(ZoneOffset offset)
添加事件的偏移量
toEpochMilli()
类似 Date类的getTime()
获取 自1970年1月1日0时0分0秒(UTC)开始的毫秒数
实例
ofEpochMilli(long epochMilli)
通过给定的毫秒数,获取Instant实例
类似 Date(long millis)
实例
编译
ZonedDateTime
一个在ISO-8601日历系统时区的日期时间
其中每个时区对应着ID,地区ID都为“{区域}/{城市}”
如
2007-12-03T10:15L30+01:00 Europe/Paris
Duration
持续时间
用于计算两个“时间”间隔
ZoneId
该类包含了所有的时区信息,一个时区的ID
如
Europe/Paris
Clock
使用时区提供堆当前即时、日期和时间的访问的时钟
Period
日期间隔
用于计算两个“日期”间隔
...
java.time.chrono
提供对不同的日历系统的访问
...
java.time.format
格式化和解析时间和日期
DateTimeFormatter
格式化或解析日期、时间
类似于SimpleDateFormat
方法图
该类提供三种格式化方法
预定义的标准格式
DateTimeFormatter.ISO_LOCAL_DATE_TIME
DateTimeFormatter.ISO_LOCAL_DATE
DateTimeFormatter.ISO_LOCAL_TIME
实例
本地化相关格式
ofLocalizedDateTime(格式参数)
适用于
LocalDateTime
格式参数
FormatStyle.LONG
FormatStyle.MEDIUM
FormatStyle.SHORT
实例
ofLocalizedDate(格式参数)
适用于
LocalDate
格式参数
FormatStyle.LONG
FormatStyle.MEDIUM
FormatStyle.SHORT
FormatStyle.FULL
实例
自定义格式
ofPattern(String parttern)
实例
格式化
日期 --> 字符串
format(TemporalAccessor t)
格式化一个日期、时间,返回字符串
TemporalAccessor
LocalDateTime
LocalDate
LocalTime
实例
解析
字符串 --> 日期
DateTimeFormatter对象.parse(CharSequence text)
实例
...
java.time.temporal
包括底层框架和拓展特性
TemporalAdjuster
时间校正器
如
将日期调整到“下个工作日”等操作
TemporalAdjusters
该类通过静态方法 提供了大量常用 TemporalAdjuster 的实现
firstDayOfXxx()
lastDayOfXxx()
nextXxx()
...
java.time.zone
包含时区支持的类
...
大多数开发者只会用到基础包和format包,也可能会用到temporal包
因此,尽管有68个新公开的类型,大多数开发者,大概只用到其中的三分之一
与传统日期处理的转换
关系图
Java比较器
概念
在java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题
Java实现对象排序的方式有两种
自然排序
java.lang.Comparable
定制排序
java.util.Comparator
Comparable
自然排序
java.lang.Comparable
概念
Comparable接口强行对实现它的每个类的对象进行整体排序
这种排序被称为类的自然排序
举例
像String、包装类等实现了Comparable接口,重写了compareTo(obj)方法,给出了比较两个对象的方式,可进行从小到大的排列
实如何实现?
实现 Comparable 类
重写 CompareTo方法
重写CompareTo(obj)的规则
如果当前对象this 大于 形参对象obj,则返回 正整数
如果当前对象this 小于 形参对象obj,则返回 负整数
如果当前对象this 等于 形参对象obj,则返回 零
实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器
实现后,如何比较排序
Collection.sort(排序对象)
Arrays.sort(排序对象)
若不实现,调用以上方法
报 ClassCastException 异常
Comparator
定制排序
java.util.Comparator
什么时候使用?
当元素的类型没有实现 Comparable 接口 而又不方便修改代码
实现了 Comparable 接口的排序规则不适合当前的操作
使用方式
Comparator c = new 实现Comparator接口的并重写compare()的类();
Collection.sort(排序对象, c)
Arrays.sort(排序对象, c)
重写compare(obj)的规则
如果当前对象this 大于 形参对象obj,则返回 正整数
如果当前对象this 小于 形参对象obj,则返回 负整数
如果当前对象this 等于 形参对象obj,则返回 零
System类
概念
代表系统
系统级的很多属性和控制方法都放置在该类的内部
java.lang.System
说明
由于该类的构造器是private的,所以无法创建该类的对象,无法实例化该类
其内部的成员变量和成员方法都是static的,所以可以很方便的进行调用
成员变量
in
标准输入流(键盘输入)
out
标准输出流(显示器)
err
标注你错误输出流(显示器)
成员方法
native long currTimeMillis()
返回当前的计算机时间(时间戳)
void exit(int status)
退出程序
0代表正常退出,非0代表异常退出
void gc()
请求系统进行垃圾回收
至于系统是否立即回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况
getProperty(特定参数)
特定参数
java.version
java运行时环境版本
java.home
java安装目录
os.name
操作系统名称
os.version
操作系统版本
user.name
用户的账户名称
user.home
用户的主目录
user.dir
用户的当前工作目录
获得系统中属性名为key的属性对应的值
实例
编译
Math类
概念
Math提供了一系列静态方法用于科学计算
其方法的参数和返回值类型一般为double型
Java.lang.Math
方法
abs
绝对值
acos,asin,atan,cos,sin,tan
三角函数
sqrt
平方根
pow(double a, double b)
a的b次幂
log
自然对数
exp
e为底指数
max(double a, double b)
min(double a, double b)
random()
返回0.0到1.0的随机数
long round(double a)
double型数据a转换为long型(四舍五入)
toDegrees(double angrad)
弧度 -> 角度
toRadians(double angdeg)
角度 -> 弧度
包含类
Biglnteger 与 BigDecimal
BigInteger
概念
java.math包的 BigInteger可以表示不可变的任意精度的整数
提供所有java的基本整数操作符的对应物,并提供java.lang.Math的所有相关方法
java.math.BigInteger
构造器
BigInteger(String val)
根据字符串构建BigInteger对象
常用方法
public BigInteger abs()
返回此BigInteger的绝对值的BigInteger
BigInteger add(BigInteger val)
返回其值为(this+val)的BigInteger
BigInteger subtract(BigInteger val)
返回其值为(this-val)的BigInteger
BigInteger multipy(BigInteger val)
返回其值为(this*val)的BigInteger
BigInteger divide(BigInteger val)
返回其值为(this/val)的BigInteger
整数相除只保留整数部分
BigInteger remainder(BigInteger val)
返回其值为(this%val)的BigInteger
BigInteger[] divideAndRemainder(BigInteger val)
返回包含(this/val)后跟(this%val)的两个BigInteger的数组
BigInteger pow(int exponent)
返回其值为(this^exponent)的BigInteger
BigDecimal
概念
在商业计算中,要求数字精度比较高,故用到BigDecimal类
java.math.BigDecimal
构造器
public BigDecimal(double val)
public BigDecimal(String val)
常用方法
public BigDecimal add(BigDecimal augend)
public BigDecimal subtract(BigDecimal subtrahend)
public BigDecimal multiply(BigDecimal multiplicand)
public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
scale
精度
小数点后多少位
roundingMode
不同的四舍五入需求
System类 与 Runtime类
System类
Runtime类
Math类 与 Random类
Math类
Random类
GUI(图形用户界面)
原教程 -【尚硅谷】Java零基础入门教程
由于个人项目重组过 导致相关链接访问可能404
由于个人项目重组过 导致相关链接访问可能404
0 条评论
下一页