java再次理解(过多的错误,只能作为错误理解吸取教训)
java泛型
泛型对于一些常用的数据结构来说非常重要,所以这方面涉及到数据结构和算法的核心
java的泛型和c++的实现原理上不一样,C++是生成新的类代码,而java是通过编译器对类型进行自动的类型检查和类型强制转换来实现的代码重用
泛型所用解决的问题就是不同类型对于代码的重用
可以重用的代码可以是一个类,一个接口,一个方法
泛型实现原理
java泛型的关键是编译器的工作
这里引入两个概念: 原始类型 和 泛型
每个泛型实际上在运行时都是其原始类型,实际上泛型的概念只出现在代码编辑阶段,在代码编译之后,不会出现任何泛型的概念
这个就是java泛型的实现机制,自动的强制类型转换
记忆口诀
泛型类需要泛型参数才能使用,实际上泛型类相比于普通类只是多了编译器对泛型参数的检查
泛型类本身是静态的,带泛型参数的泛型类是同一个泛型类,只是编译器检查时具体的比对点不同
泛型类的坑:
泛型参数在内部实际上就是object,不能用来new 泛型参数(java防止误解所以禁止)
静态的变量和方法不与泛型参数相关,在生命周期上看,现有静态后来才有泛型参数
只承认className<?>类型,使用instanceof的时候只能使用<?>的这个才是这一排泛型的真身
java流式编程
java的语法一直都随着现代编程语言的发展在更新,例如现在的函数式编程,流式编程,面向对象编程,lambda表达式,并发编程,这些东西语法一直都在更新,所以这些语法我们可以看到其他语言的影子,我们在学过scala,spark之后对于流式编程不可谓不熟悉,在spark中,两个算子,转换算子和行动算子.这可以算得上是流式编程的最重要的应用了.
流式编程不可离开的就是lambda表达式,而lambda表达式实际上是函数式编程的一部分.
函数式编程
所谓函数式编程就是此函数没有状态,同样的输入一定会得到同样的输出.
java对函数式编程的支持就是函数式接口注解
@FunctionalInterfase
加上这个注解之后,接口中只能有一个方法,不能有多余的方法和状态的定义
Lambda表达式
实际上就是定义函数的一个快速语法糖
实际上编译器检测到lambda表达式语法,会根据表达式生成一个类的静态函数接口实例,然后再调用这个实例的方法
所以lambda表达式实际上不能使用实例对象中的变量,只能使用类的静态变量(注:大错特错,详细请看我的csdn上的,lambda表达式本章。以前的理解
实际上lambda表达式是编译器动态生成的静态方法,在源代码中,可以在lambda表达式中引用外部的常量,例如String,Integer,final变量等,在编译器检测到的时候,实际上就拷贝了一份在静态方法中,看似是引用实际上是两种不同的东西.
基本语法
()->{}
one->one
函数名
流式编程
在java中流式编程主要是用来处理collection集合类的,像list,set等都是collection的子类
java 提供了一个接口API Stream 接口,数组通过Arrays.toStream()可以得到stream,collection类植入了stream方法得到集合的stream
流式编程的两个要点就是数据源+数据终端 以及 中间操作
- 从数据源中获取数据流
数组,集合中都可以获取数据流,产生数据流操作并不会真影响数据源中的数据
实际上可以将这个操作看成是通过迭代器读取了一个数据出来,并不会直接拿到数据的引用
2.
java 数据结构实现类
列表和队列
列表
Java中常用的列表有ArrayList,LinkedList这几个实现类
这些类之所以可以叫做列表是因为实现了List接口,这个接口提供了列表必须的几个方法
add方法,添加元素
isEmpty确认是否为空
get(int index)
indexOf(Object )
remove(index)
等方法
这列表实现类的用法几乎相同,就是底层实现的原理不同
ArrayList
这个实现类是最为常用的数据结构之一
实现了Iterator,List,Collection,RandomAccess等接口
实现是通过数组实现的
LinkedList
这个实现类也是非常常用,不过和ArrayList不一样的是,这个实现类常常被用作队列来使用,主要是链表的删除和插入效率很高,队列不会涉及随机读取内部的数据,所以这个实现类完美适配队列这种结构
实现了Queue接口,Deque接口,List,Collection
ArrayDeque
我们知道,实际上数组也可以实现队列通过%运算可以实现,也就是循环数组
这个是实现类实现类Deque接口
Queue 接口
java中的Queue实际上是一个接口,有不同的实现类实现了这个接口,比如
- LinkedList 最常用的实现类
- PriorityQueue
接口方法
三类方法:
- 加入队列
- 弹出队列
- 获取元素
加入队列
加入队列有两个方法,区别在于对于处理满栈的方式
- add()方法
如果加入不了就抛出异常,add方法要求一定能加入队列
- offer()方法
提供加入队列的机会,但是不保证能够加入,如果队列满,会返回false
弹出队列
也有两种,区别在没有可移除的对头的时候是否报错
- poll() 移除并返回
最常用的移除方法,不会报错,返回false
- remove() 必须移除
这个方法要求必须能移除一个元素,否则报错
查询元素
查询元素不会将元素移除
也是两种方法,区别在于查询不到元素的时候是否报错
- element()
这个方法要求一定要能够返回对头元素,如果没有,会报错
- peek()
这个方法只是一个动作描述,如果没有对头元素返回null
我的解释
实际上我们从来不是说只用queue,而是让实现类满足了这个queue接口,所以实际上的实现类除了queue接口所要求的方法之外,像collection接口要求的方法很可能也实现了,所以不仅仅是这几个方法,在实际使用过程中可以灵活使用这些方法,实现对于队列的灵活操作
为什么使用LinkedList而不是ArrayList?
因为队列一端进一端出,这和链表的数据结构设计非常符合,而是用ArrayList有点浪费,但是也可以用,不过ArrayList只是没有提供接口的实现而已,实际上可以通过自己合理的方法调用,逻辑上实现queue功能
Stack<E>泛型类
这个是一个具体的实现类,提供了栈这个数据类型所需要的各种方法
因为栈可以通过数组,链表等常见数据结构模拟实现,效率往往还不低,所以这个实现类Stack<?>比较少使用
方法
加入元素
push(value)
弹出元素
pop()
查询元素
peek()
常用操作
isEmpty()判断栈是否为空
size()返回中元素的个数
Map映射表和Set
这个数据结构在DP,记忆化搜索之类的算法中非常有用
在java中MAP是一个接口,有不同的实现类实现了这个接口
常用的有HashMap
和其他常用数据结构数据传递
List,Collection和Array的数据传递
Array转List,分为两种
1) 通过工具类,为原数组一个类似List接口的窗口对象,实际上这个返回的是java.util.Arrays.ArrayList
,它并非java.util.ArrayList
类。如果修改会报错 具体操作:Arrays.toList(arrayObject)如果想修改只能通过原数组对象进行修改,不能通过代理对象进行这种跨界操作(代理而不是全权控制)
1 |
|
2) 另一种就是List的数据copy自Array,1 通过List的构造方法,可以传入上述Arrays.ArrayList对象作为数据源 2 通过Collections.addAll(集合对象,数组对象)需要注意先把集合对象初始化size到数组长度,然后注意这个是二进制复制 3
1 |
|
List接口转换Array,List接口中提供的方法toArray包括无参和传参两种,无参和有参都不会让内部引用直接暴露,而是复制一份。无参每次调用都会返回新的Object[],而传参会将数据复制到指定大小的同类数组中。
Java中的日期和时间类
专门讲解java中的时间类是因为,Java对于时间的处理有两套
在Java8之前,java的时间的标识和处理被认为设计的很烂,所以在java8之后,java提供了一套新的时间处理API
在讲java提供的具体时间API之前,我们需要先了解时间在程序中究竟是怎么被理解和使用的
日期时间在程序中的理解和使用
在程序中,时间这个概念和物理上和常识上可能都不相符合
程序中的时间指的是天时,表示的是在某地的一天24小时中,处于哪个时间点
时间戳 表示真正不考虑地理,纯粹处于同一时刻的表示
所以在同一个时刻,也就是同一个时间戳下,不同地区的本地时间不同
所以记住,在程序中,时间日期都指的是本地时间和日期
时间戳在任何地方都表示同一个时刻
需要的概念
- 时间(本地时间)
- 时间戳(时刻)
- 时区(Timezone 同一时刻在不同的时区,所处的时间不同)
- 时间的表示
- 日期
- 日期表示
- 时期和时间
- 时期和时间的表示
提供的操作
- 时间和时间戳通过时区进行相互转换
- 字符串解析为日期和时间,以及时间转成字符串的各种形式
- 各种操作
Java 新提供的API
1. 表示时刻(时间戳) Instant
类名Instant 意思是时刻的意思
获取对象方法
一般是通过静态工厂方法获取到对象的
- 通过无参静态方法now()获取对象
Instant.now()
- 传入时间戳的静态方法
Instant.ofEpochMilli()
意思是 of 纪元时
2. 表示本地日期时间
类 LocalDateTime
注意这个类不含时区信息,也就是说这个类中的时间信息无法直接转换成时间戳
获取对象方法
也是通过静态方法获取对象的
- 无参静态方法
LocalDateTime.now()
默认获取本地时间
- 传入时间参数的静态方法
LocalDateTime.of(2022,1,1,23,56,59)
也就是年月日,时分秒的顺序
常用操作
获取年月日,在星期几,等等
3. 表示时区
有两种方式来表示一个一个时区
一种是使用id表示每一个时区 叫做ZoneId
另一种是时区相对于标准时区偏移的时间来表示一个时区 叫做 ZoneOffset
他们俩实际上是可以相互转换的,但是使用ZoneOffset可能更简便
类名 ZoneId 和 ZoneOffset
实际上ZoneOffset是ZoneId的子类
这个类就是用来表示时区的,通过时区id表示一个时区,实际上时区是通过相对于标准时区的偏移作区分的
标准时区就是格林尼治时间的时区,我们的北京时区就是+8:00表示相对于格林尼治时间偏移8小时
时区的ZonId表示例如:
北京时间 ZoneId.of(“GMT+8:00”)
而 ZoneOffset 的表示就是
北京时间 ZoneOffset.of(“+8:00”)
可见ZoneOffset确实要简单一点
获取对象
- 静态方法 of 参数是字符串
ZoneOffset.of(“+8:00”)
ZoneId.of(“GMT+8:00”)
- 获取默认时区(本地时区)
ZoneId.systemDefault()
4. 表示时区+时间
有了时区信息,时间戳信息,本地时间信息,三者之间有两者可求得第三者
类 ZonedDateTime
实际上就具备了所有的时间要素,对象中保存了本地时间信息,本地时区信息,当然也能从而得知Instant时间戳信息
获取对象
也是静态方法获取对象
- 获取当前时区本地时间
ZonedDateTime.now()
- 传递参数构建对象
ZonedDateTime.ofInstant(Instant,ZoneId)
根据时间戳加上时区…
ZonedDateTime.of(LocalDate,LocalTime,ZoneId)
根据本地时间加上时区
主要操作
- 获取时间戳信息
toInstant()
5. 字符串表示与解释
java8新的API这个格式化类主要优化的方向就是线程安全
java.time.format.DateTimeFormatter
主要提供的格式化方式有下面几种
- 提供pattern模式匹配格式化
最常用的就是
formatter = DateTimeFormatter.ofPattern(“yyyy-MM-dd HH:mm;ss”);
这种格式对象就会将 年月日,时分秒信息对应替换掉模式字符串中相应的字段中去
2024-1-28 23:23:23
- 使用提供好的静态对象
一般就是ISO的时间格式的格式化对象
对象的操作
- 格式化操作
给一个时间类,返回一个字符串
formatter.format(LocalDateTime.now())
- 解释字符串
给一个字符串,格式化类试图解析出时间信息,返回一个LocalDateTime对象
通常是给时间类的parse方法作为参数传递
例如
LocalDateTime.pasrse(“2024-1-1 09:12:12”, formatter);
Java中的一些实用接口
Iterable以及Iterator
Iterable可迭代
这个接口只需要一个方法
iterator() 返回一个Iterator接口
也就是说可迭代指的是拥有迭代器就可以了
Iterator迭代器
迭代器是真正实现迭代的接口
hasNext-next-remove等方法是这个接口索要提供的迭代所需要的方法
可以提一下这个remove方法,为什么是必要的,当我们使用迭代器遍历元素的时候,想要删除某一个元素,如果直接通过数据结构的remove删除了,此时迭代器并不知道某个元素被删除了,ok他继续通过下标进行迭代,但是如果删除的元素在当前迭代元素的前方,那么所有后面的元素会向前移动一位,有一个元素就漏掉了没迭代到,其他情况也会导致问题
所以当我们要在迭代器迭代的时候删除某一个元素,务必使用迭代器接口中提供的remove方法来改变,这样迭代器也会对应做出修改,确保后面的迭代不会出现问题
ListIterator列表迭代器
这个接口提供了更多具体的方法
hasPrevious()
向前遍历previous
获取下一个元素的下标nextIndex()
获取上一个元素的下标peviousIndex()
set()
add()
迭代器的实现原理
对于ArrayList中对于Iterator的实现是通过一个内部类Ltr implements Iterator
通过三个成员变量
int current //下一个要返回的元素位置
int lastRet = -1 表示最后一个返回的索引位置,没有就使用-1值
int exceptedModCount = modeCount //用于检测是否发生结构性变化
外部类中有一个变量就是modCount,每当发生结构性变化这个值就会+1,而每次迭代Iterator就会去检测是否和外部的modCount是一致的,一致的就说明没有发生结构性变化,如果发生了结构性变化而Iterator不知道,就会导致这exceptedModCount与外界不一致,此时Iterator会抛出异常
迭代器的作用
迭代器体现的是一种关注点分离的设计思想
将数据的实际组织形式和迭代遍历相分离
也就是说我们提供了统一的接口方便实用,而每个数据结构跟句子的实际的数据组织形式,提工作高效的数据便利的方法通过Iterator
RandomAccess接口
这是一个声明式接口,表示数据是连续存储的,可随机访问的,ArrayList就声明了这个接口
一些通用代码会根据是否声明这个接口,选择是否采取更加高效的算法
例如Collections中的binarySearch就会判断传入的Collection是否声明这个接口
Queue接口
这个接口实际上是继承自Collection接口,进行了扩展,所以原本的collection接口的方法都有
额外提供的方法是:
尾部添加元素操作 add 和 offer
查看首部元素操作 element 和 peek
删除头部元素操作 remove 和 poll
左边的方法是不会处理异常的,右边的会对异常情况进行处理,给出一个合理的结果
例如add方法如果没有多余的空间,元素就无法加入到队列,此时会抛出异常
而offer如果发现没有空间了,加不进去就会返回false表示无法提供offer
类似的peek和poll会返回null
Deque接口
这个接口是为了提供Stack栈的功能,为什么不叫做stack呢,因为实际上栈的功能包含在双端队列之中了
没必要在开发一个Stack接口,不过确实有一个类叫做Stack
双端接口继承自Queue,也就是对Queue接口进行了拓展
提供的方法
主要是这三个:
push 从头部添加元素,会抛出异常
pop 这个Queue也提供了,是重复的
peek 这个Queue也提供了,是重复的
除了这三个方法,还提供了一些方便双端操作的方法
1 |
|
Map与Set接口
MAP
每个元素是一个键值对,这个就是MAP
键值对可以使用Entry类表示
Comparable以及Comparator
comparable这个接口是判断一个类是否支持Arrays等java提供的工具类能否自动排序的特征
这个接口包含方法 int compareTo(T o)方法
而Comparator是第三方比较类,而不是类自带的比较器。所以实际上可以有很多的第三方比较器。
提供的方法是int compare(T o1, T o2)
Comparator还提供了各种排序策略,例如升序,降序,内置的排序方法都提供了接受参数的方法
Java的输入输出流
最基本的定义
最顶层的就是输入输出流,inputStream和outputStream
输入流可以从中获取到数据,输出流可以输出数据进去,就是这个定义
不同的液体(粒度)
java提供的流大致有两种,一种是字节流,一种是字符流
实际上字符流就是字节流进行加工得到的,但是因为字符流非常常用,所以java就将其作为基础流类型与字节流处于同一级别下了
字节流是所有输入输出流都提供的一种方式,字节流是按照一定的编码标准,将字节流翻译成字符形成的流
java提供了不同的名字来区分这两种粒度:
- 以stream结尾的流,一定是字节流
- 以Reader或者Writer结尾的流就是字符流
最常见的流端口类型
解决流从哪里来,到哪里去的问题
最常见的输入输出流是来自file,也就是文件的输入输出
FileInputStream
FileOutputStream