文章目录
-
- 面试必问
-
- Java基础
-
-
- **Java一次编写到处运行**
- **Java的数据类型**
- Java语言的编码方案
- Java的访问权限
- this关键字
- super
- 全局变量和局部变量
- static的主要意义
- Java跳出多重循环(ok: break ok;)
- 为什么要有包装类?
- Integer与Double如何进行比较?
- Integer与int的区别,二者在进行==运算时得到什么结果
- 封装的目的是什么?为什么会有封装?
- 多态的理解?
- Java为什么是单继承?
- 重载和重写的区别?
- 构造方法可不可以重写?
- Object类中的方法
- hasCode()和equals()
- ==和equals()
- String、StringBuffer和StringBuilder的区别
- 使用字符串时,new和“”更推荐哪种方式?
- 字符串拼接的方式
- 字符串`String a = "abc";` ,说一下这个过程会创建什么,放在哪里?`new String("abc")` 是去了哪里,仅仅是在堆里面吗?
- 接口和抽象类的区别
- 处理异常的方式
- Java的异常接口
- Static关键字的理解
- 对泛型的理解
- 对Java反射机制的理解
- Java反射在实际项目中应用场景有?
- Java的四种引用方式?
- 集合类
-
- Java中的集合类(容器)?
- Java中的容器,线程安全和线程不安全的分别有哪些?
- Map接口有哪些实现类?
- 简述Map put的实现过程
- 如何得到一个线程安全的Map?
- JDK7和JDK8中的HashMap有什么区别?
- HashMap的底层原理
- 为什么HashMap线程不安全?
- HashMap使用红黑树不使用b/b+树的原因
- HashMap和ConcurrentHashMap有什么区别?
- 对LinkedHashMap的理解
- TreeMap的底层原理
- Map与Set的区别?
- List和Set的区别?
- ArrayList和LinkedList的区别?
- 线程安全的List
- CopyOnWriteArrayList的原理
- TreeSet和HashSet的区别
- Stream(不是IOStream)有哪些方法?
- 简述Java中的IO流
- 怎么用流打开一个大文件?
- NIO的实现原理
- Java的序列化与反序列化
- Serializable接口为什么需要定义serialVersionUID变量?
- 除了Java自带的序列化之外,还了解哪些序列化工具?
- 并发
-
- 什么是线程安全?
- 保证线程安全的策略?
- 如何在两个进程间共享数据
- 创建线程的方式有几种?
- Thread类的常用方法
- 为什么wait(),notify(),notifyall(),不在Thread类中
- run()和start()有什么区别
- 线程是否可以重复启动会有什么后果?
- 介绍一下线程的生命周期?
- 在Java中守护线程和用户线程有什么区别?
- 什么是多线程的上下文切换
- 如何实现进程同步?
- Java多线程之间的通信方式
- sleep()和wait()的区别
- 如何让子线程先执行,主进程再执行
- 阻塞进程的方式
- 线程中阻塞和唤醒
- Java中用到的线程调度算法是什么?
- Synchronized和Lock的区别
- Synchronized的底层实现原理
- synchronized可以修饰静态方法和静态代码块吗?
- 谈一谈ReetrantLock的实现原理
- 如果不使用Synchronized和Lock如何保证线程安全?
- 说一说Java中乐观锁和悲观锁的区别?
- 公平锁和非公平锁是怎么实现的?
- 什么是CAS操作,缺点是什么?
- 了解过Java里面的锁升级嘛?
- 如何实现互斥锁(mutex)?
- 分段锁是怎么实现的?
- 说一说对于读写锁的了解
- 谈一谈对于JUC的理解
- 介绍下ThreadLocal和他的应用场景
- 在Java中Executer和Executers的区别
- 介绍一下线程池
- 介绍一下线程池的工作流程
- JVM
- Java web开发
-
- SpringBoot的理解
- SpringBootStarter有什么用?
- SpringBoot的启动流程?
- 来描述一下SpringBoot自动装配的过程
- 说说你对SpringBoot注解的了解
- Spring的核心是什么
- AOP的应用场景
- SpringAOP不能对哪些类进行增强
- 谈一谈Spring容器的了解
- 谈一谈BeanFactory的了解
- Spring是如何管理Bean的?
- 介绍一下Bean的作用域
- 说一说Bean的生命周期
- Spring是怎么解决循环依赖的?
- @Autowired与@Resource注解有什么区别?
- Spring中默认提供的单例是线程安全的嘛?
- Spring是如何管理事务的
- Sping的事务传播方式有哪些?
- Spring的事务如何配置、常用注解有哪些?
- SpringMVC
- MyBatis
-
- 谈谈MyBatis和IPA的区别
- MyBatis支持的输入输出类型有哪些?
- MyBatis里如何实现一对多关联查询?
- MyBatis中的$和#有什么区别
- MyBatis的xml文件和Mapper接口是怎么绑定的?
- MyBatis分页和自己写分页哪个效率高
- 了解MyBatis的缓存机制吗
- cookie和session的区别是什么?
- cookie和session各自适合的场景是什么
- session的工作原理
- get和post请求有什么区别
- get请求的参数能放到body里嘛?
- 页面报400错误是什么意思?
- 请求数据出现乱码怎么处理?
- 如何在SpringBoot框架下实现一个定时任务?
- 调用接口时要记录日志,该怎么设计?
- 了解SpringBootJPA嘛?
-
- MySQL
- TCP/IP协议
- TIME_WAIT和CLOSE_WAIT
- 操作系统
- 设计模式
- 设计模式
面试必问
Java基础
Java一次编写到处运行
JVM(Java虚拟机),源代码(.java)由编译器编译为字节码(.class)。将字节码文件放到各个平台的虚拟机运行。
Java的数据类型
包括基本数据类型和引用数据类型,基本数据类型有,byte,short,int,long,float,double,char,boolean。引用数据类型(类,接口,数组),就是对一个对象的引用,根据引用对象类型的不同进行分类。本质上通过指针,指向堆中对象所持有的内存空间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-26vWxGI2-1639467004337)(C:\Users\86130\AppData\Roaming\Typora\typora-user-images\image-20211204153047326.png)]
Java语言的编码方案
采用Unicode编码标准,为每一个字符制定了唯一一个数值,在任何的语言、平台、程序都可以放心的使用。
Java的访问权限
三种访问修饰符:private、protected、public
四种访问权限:private、default、protected、public
修饰成员变量/成员方法时,
- private:类内部成员可以访问
- default:类内部成员、同一包下的其他类访问
- protected:类内部成员、同一包下的其他类,他的子类
- public:任一包下的任一类。
修饰类,只能由两种访问权限
- default:同一包下的其他类
- public:任一包下的任一类访问
final、final、finalize的区别
- 被final修饰的类不可继承
- 被final修饰的方法不可重写
- 被final修饰的变量不可被改变(这里改变的是指引用而不是指向的内容)
finally一般是作用于try-catch代码块中,在处理异常时,将一定会执行的代码块放入finally中(一般存放一些关闭资源的代码)
finalize是一个方法,属于Object类的一个方法,Object是所有类的父类,该方法一般由垃圾回收器调用,当我们调用System.gc()方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
this关键字
指向对象本身的指针
super
指向自己父类的指针(这个超类是指的是;离自己最近的父类)
以上两个关键字均不适用于static环境
全局变量和局部变量
全局变量在类的范围内定义的变量,有默认初始值;局部变量是方法里定义的变量。
static的主要意义
是能够没有创建对象也能使用属性、方法,形成静态代码块优化程序性能:只在类加载的时候执行一次。因此很多只需执行一次的初始化操作都放在static代码块中进行。
Java跳出多重循环(ok: break ok;)
为什么要有包装类?
Java设计理念是“一切皆对象”,所有的引用类型的变量都继承于Object类,都可以当做Object类型的变量使用,但基本数据类型却不可以。如果某个方法需要Object类型的参数,但实际传入的值却是数字的话,就需要做特殊的处理了。有了包装类,这种问题就可以得以简化。
自动封装:可以直接将一个基本数据类型的数据赋给对应的包装类型。
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本数据类型。
Integer与Double如何进行比较?
- 不能用==,因为是不同数据类型
- 不能转化为字符串,浮点数带小数点,整数值不带
- 不能用compareTo方法比较(只能比较相同类型)
- 可以都转化为相同的基本数据类型再进行比较
Integer与int的区别,二者在进行==运算时得到什么结果
二者再进行比较时,Integer会自动拆箱为int在进行比较
封装的目的是什么?为什么会有封装?
隐藏类的实现细节,限制外部对成员变量的访问,可以进行数据检查,有利于保证对象信息的完整性,便于修改提高代码的可维护性。
多态的理解?
Java里面允许把一个子类对象直接赋给一个父类引用变量,无需任何类型转换(向上转型)。
相同类型的变量、调用同一个方法时呈现出多种不同的行为特征就是多态。
继承、重写、将子类对象赋给父类引用,多态的实现方式重写(重写父类的某些方法)、接口、抽象类(实现抽象类/接口的某些抽象方法)。
Java为什么是单继承?
Java在设计时借鉴了C++的语法,C++支持多继承,如果,两个父类包含相同的方法,子类在调用或重写这个方法时就会产生迷惑。Java中的一个类只能有一个直接的父类(可以有多个间接的父类)。
重载和重写的区别?
重载只能发生在一个类中,方法名相同、参数列表不同。(重载的方法不能根据返回值进行区分)。重写,重写发生在子类和父类之间,方法名必须和父类方法一致,返回值、异常要小于等于父类方法,访问权限要大于父类方法。
构造方法可不可以重写?
构造方法不能重写。构造方法要求与类名保持一致,如果子类可以重写构造方法,意味着,子类中必然出现与其类名不相同的构造方法,这与构造方法的要求显然是矛盾的。
Object类中的方法
Class<?>getClass:返回对象运行时的类。
boolean equals(Object obj):判断指定对象与该对象是否相等。
int hasCode():返回该对象的hasCode值:类似于哈希?
String toString():返回可以表述该对象信息的字符串
wait()、notify()、notifyAll()控制线程的暂停与运行,clone()获得当前对象的副本(被protected修饰)、finalize()垃圾回收方法
hasCode()和equals()
hasCode用于获得哈希码,equals用于比较两个对象是否相等。
两个对象相等必须有一样的哈希码,两个对象有相同的哈希码,未必相等
==和equals()
==作用于基本运算符时,比较两个数值是否相等,作用于引用数据类型时比较两个对象的内存地址是否相同,判断他们是否为同一对象。
equals()没有重写时,Object默认用==实现,比较内存地址是否相同,进行重写之后,按照内容进行比较
String、StringBuffer和StringBuilder的区别
String是不可变类,由final修饰,不可被继承,不可被修改。一个String对象一旦被创建,不可被改变,直至被销毁。
StringBuffer对象代表字节序列可以改变的字符串,可以通过自带的方法如(append、setCharAt)来改变字符串序列。如果生成了最终想要的字符串,可以调用toString()方法将其转化为String对象。
StringBuilder和StringBuffer都代表着可变的字符串对象,并且他们都具有相同的父类,构造方法与成员方法基本相同。不同的是StringBuffer线程安全,一般优先考虑。
使用字符串时,new和“”更推荐哪种方式?
采用new的方式会多创建一个对象出来,占用更多内存,一般使用直接量的方式创建字符串。
字符串拼接的方式
“+”:要求拼接的都是字符串常量,在编译器编译时
直接会优化成为一个完整的字符串。StringBuffer要求线程安全,底层采用append()方法实现。concat方法拼接两个字符串时效率高。
字符串String a = "abc";
,说一下这个过程会创建什么,放在哪里?new String("abc")
是去了哪里,仅仅是在堆里面吗?
JVM使用常量池来管理字符串直接量,会先检查常量池,if(1)赋给a,else先将“abc”存入常量池,再进行赋值;在执行的时候,会先将abc存入常量池,再在堆内存中创建String对象,最后再将堆内存中变量a指向常量池中变量“abc”。
接口和抽象类的区别
设计的目的上,二者的区别:
接口本质上是一种规范,对于实现者而言,规定了必须向外提供哪些服务,对于调用者来说规定了可以调用哪一部分功能。是多个程序之间通信的标准。抽象类是一种模板设计,是系统实现过程中的中间产品。
接口(can)、抽象类(is-a)
接口只能包含抽象方法,静态方法,默认方法和私有方法,不能为普通方法提供方法实现,抽象类包含普通方法。都不能被实例化。
接口中不能包含构造器和初始化块定义,只能定义静态变量。
一个类只能有一个直接父类包括抽象类,但是一个类可以实现多个接口,通过接口可以弥补Java单继承的不足。
通过对接口的使用可以很好的降低程序各模块之间的耦合,提高系统的可扩展性和可维护性。
处理异常的方式
try-catch-finally
将业务代码封装进try块内部,发生任何异常都会创建一个异常对象,将此异常对象交给catch块处理。
catch先记录日志,根据异常类型,结合业务情况进行相应处理。
打开了某个资源,比如数据库连接等,无论是否发生异常,都需要执行的代码块可以放在finally中
抛出异常:当判断某项错误的条件成立时可以使用throw向外抛出异常,不知道如何处理时,可以使用throws关键字抛出异常交由JVM处理
Finally语句块总会执行,注意Finally代码块中不要使用return,throw等使程序终止的语句,否则将会导致trycatch中使程序终止的语句失效。
Java的异常接口
Throwable是异常的顶层父类,代表所有的非正常情况。两个直接子类(Error(无法恢复,不能被捕获),Exception(可以使用Try-catch捕获))
Static关键字的理解
Static修饰的类可以被继承,static关键字可以修饰成员变量,成员方法,初始化块,被static修饰的成员是类的成员,属于类,不属于单个对象。被修饰的代码块,在类加载的时候被隐式调用一次,之后便不会被调用了。可以不经过实例化直接使用方法和变量,形成静态代码块,只在类加载时执行一次提高效率。
对泛型的理解
声明一个属性或者是变量的时候不设置其具体的数据类型,而是在指定结构使用的时候才进行动态的配置,而使用泛型主要是为了解决强制类型转换的设计缺陷。
对Java反射机制的理解
编译期和运行期
是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。简单来说,反射机制指的是程序在运行时能够获取自身的信息。在 Java 中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。
![img](https://i-blog.csdnimg.cn/blog_migrate/020b8458b71eef1ca17606a059073171.png)
Java反射在实际项目中应用场景有?
- JDBC,创建数据库连接时,需要通过反射机制加载数据库的驱动程序。
- 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化
- 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现
Java的四种引用方式?
- 强引用:程序创建一个对象,把这个对象赋给引用变量,程序通过引用变量操作实际的对象,它属于可达状态,不可能被系统垃圾回收机制回收。
- 软引用:通常用于对内存敏感的程序中,内存足够不会回收,内存不足立刻回收。
- 弱引用:只要系统垃圾回收机制运行时才会被回收
- 虚引用:虚引用完全类似于没有引用,虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列联合使用
集合类
Java中的集合类(容器)?
Java中的集合类主要由Collection和Map这两个接口派生而出,其中Collection接口又派生出三个子接口,分别是Set、List、Queue。所有的Java集合类,都是Set、List、Queue、Map这四个接口的实现类,其中Set(代表无序的,元素不可重复的集合),List(代表有序的元素可以重复的集合),Queue(代表先入先出的队列),Map(代表有映射关系(key-value)的集合)
现在常用的实现类有HashSet,TreeSet,ArrayList,LinkedList,ArrayQueue,HashMap(线程不安全k,v允许为null),TreeMap
Java中的容器,线程安全和线程不安全的分别有哪些?
Java.util包下的容器大多是线程不安全的,像我们常用的HashSet,TreeSet,ArrayList,LinkedList,ArrayQueue,HashMap,TreeMap大都是线程不安全的集合类,但是优点是性能好。可以使用Collections工具类提供的synchronizedXxx()方法,将这些集合类包装成线程安全的集合类。Vector,Hashtable线程安全但是过于古老。
以Concurrent开头的集合类:支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。
以CopyOnWrite开头的集合类:采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。
Map接口有哪些实现类?
比较常用的有HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap。在不需要排序的场景优先使用HashMap,是最好的Map实现,如果要保证线程安全可以使用ConcurrentHashMap。
需要排序的场景,按照插入顺序排序可以使用LinkedHashMap,按照key排序可以选择TreeMap。保证线程安全可以用collection类包装。
简述Map put的实现过程
HashMap是最经典的Map实现,根据他的视角介绍put
- 首先判断数组是否为空,若数组为空则进行第一次扩容(resize)(负载因子);
- 通过hash算法,计算键值对在数组中的索引;
- 插入数据
- 当前位置元素为空,直接插入数据
- 当前位置元素非空,key已存在,直接覆盖value
- 当前位置元素非空,key不存在,则将数据链到链表末端
- 如果链表长度达到8,则将链表转化为红黑树,并将数据插入到书中
- 再次扩容(如果元素个数超过threshold)
如何得到一个线程安全的Map?
- 使用Collections工具类包装
- 使用java.util.concurrent包下的Map
- 不建议使用Hashtable性能较差(Hashtable不允许使用null作为key和value,如果试图把null值放进Hashtable中,将会引发空指针异常,但HashMap可以使用null作为key或value。)
JDK7和JDK8中的HashMap有什么区别?
JDK7中的HashMap是基于数组+链表实现的,hash冲突的解决方法,直接将计算出来的KV键值放到已有元素后面,因此hash冲突严重时,会大大增加时间复杂度
JDK8是基于数组+链表+红黑树实现的,大于8时自动转化为红黑树,大大提高查找效率
HashMap的底层原理
基于hash算法,通过put方法和get方法存储和获取对象。
存储对象时:K/V->put方法,调用K的hasCode计算hash得到bucket位置,并进一步使用equals()方法确认键值对。在发生碰撞时,HashMap通过链表将产生冲突的元素组织起来,如果超过了8,用红黑树替代链表提高查找效率
为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时,会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时,又会将红黑树转换回单向链表提高性能。
为什么HashMap线程不安全?
HashMap在并发执行put操作时,可能会导致形成循环链表,从而引起死循环。
在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。
HashMap使用红黑树不使用b/b+树的原因
b+树和b树多用于外存中,是一种磁盘友好的数据结构,如果使用B/B+树在数据量很少的情况下,查找效率和链表几乎一样。
HashMap和ConcurrentHashMap有什么区别?
HashMap是非线程安全的,多线程进行操作时,可能会产生数据不一致的情况,还有可能因为并发插入元素形成环,在查找的时候就会发生死循环,影响整个程序。
Collections工具类可以包装成线程安全的,基于synchronized关键字保证线程安全,底层基于互斥锁,性能和吞吐量较低。ConcurrentHashMap的实现较为复杂,性能高很多,没有使用全局锁锁住自己,采用了减少锁粒度的方法,尽量减少因为竞争锁导致的阻塞与冲突,ConcurrentHashMap的检索操作是不需要锁的。
在JDK1.8中ConcurrentHashMap是直接使用Node数组+链表+红黑树的数据结构进行实现的,并发控制使用了Synchronized和CAS来操作,整个就像是优化且线程安全的HashMap
对LinkedHashMap的理解
采用双向链表维护k-v对的顺序,可以避免对HashMap,Hashtable里面的k-v对进行排序,只需要在插入的时候维持顺序即可,又可避免使用TreeMap所增加的成本。LinkedHashMap需要维护元素的插入顺序,性能略低于HashMap,但是因为有链表来维持内部顺序,在迭代访问Map中全部元素时有较好的性能。
LinkedHashMap继承于HashMap,在HashMap的基础上,通过维护一条双向链表,解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题,与hashMap几乎一致除了重写的维护双向链表的部分方法。
TreeMap的底层原理
TreeMap是基于红黑树实现的,映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator进行排序,具体情况取决于使用的构造方法。TreeMap的基本操作containsKey,get,put,remove时间复杂度为log(N),其中root为红黑树的根,size是红黑树的节点个数,在Entry节点中key的大小是根据比较器comparator进行判断的。
Map与Set的区别?
Set是无序的、元素不可重复的集合
Map代表代表具有映射关系(key-value)的集合,其所有的key是一个Set集合,key无序且不能重复。
List和Set的区别?
Set代表无序元素不可重复的集合;List代表有序元素可以重复的集合。
ArrayList和LinkedList的区别?
ArrayList的实现是基于数组,LinkedList的实现是基于双向链表;随机访问的效率,ArrayList优于LinkedList;插入和删除操作则相反;LinkedList比ArrayList更消耗内存
线程安全的List
Vector:古老,线程安全,但是效率低
Collections.SynchronizedList:相较于vector有更好的扩展性和兼容性,所有方法都带有同步锁,也不是最优的List
CopyOnWriteArrayList:采用复制底层数组的方式进行操作,读取会直接读取集合本身,无序加锁和阻塞,由于对集合的写入操作都是对数组的副本的执行,因此线程安全,性能也最优。
CopyOnWriteArrayList的原理
CopyOnWriteArrayList是Java并发包里提供的并发类,简单来讲,就是线程安全且读操作无锁的ArrayList,在执行写操作时会复制一份新的List,在新的List上完成写操作,并将原引用指向新的List,保证了写操作的进程安全。
TreeSet和HashSet的区别
HashSet、TreeSet的元素都是不能重复的,并且他们都是线程不安全的。
HashSet的元素可以是null,但是TreeSet不可以;HashSet不能保证元素的排列顺序,TreeSet可以支持自然排序和定制排序。
HashSet底层采用哈希表实现,TreeSet底层采用红黑树实现。
HashSet的底层结构,基于HashMap实现,默认构造函数是初始化初始容量为16,负载因子为0.75的HashMap。
Stream(不是IOStream)有哪些方法?
Stream提供了大量的方法进行聚集操作,这些方法既可以是“中间的”,也可以是“末端的”。
- 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的map()方法就是中间方法。中间方法的返回值是另外一个流。
- 末端方法:末端方法是对流的最终操作。当对某个Stream执行末端方法后,该流将会被“消耗”且不再可用。上面程序中的sum()、count()、average()等方法都是末端方法。
除此之外,关于流的方法还有如下两个特征:
- 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
- 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。
下面简单介绍一下Stream常用的中间方法:
- filter(Predicate predicate):过滤Stream中所有不符合predicate的元素。
- mapToXxx(ToXxxFunction mapper):使用ToXxxFunction对流中的元素执行一对一的转换,该方法返回的新流中包含了ToXxxFunction转换生成的所有元素。
- peek(Consumer action):依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法主要用于调试。
- distinct():该方法用于排序流中所有重复的元素(判断元素重复的标准是使用equals()比较返回true)。这是一个有状态的方法。
- sorted():该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
- limit(long maxSize):该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。
下面简单介绍一下Stream常用的末端方法:
- forEach(Consumer action):遍历流中所有元素,对每个元素执行action。
- toArray():将流中所有元素转换为一个数组。
- reduce():该方法有三个重载的版本,都用于通过某种操作来合并流中的元素。
- min():返回流中所有元素的最小值。
- max():返回流中所有元素的最大值。
- count():返回流中所有元素的数量。
- anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合Predicate条件。
- noneMatch(Predicate predicate):判断流中是否所有元素都不符合Predicate条件。
- findFirst():返回流中的第一个元素。
- findAny():返回流中的任意一个元素。
Java 8还为上面每个流式API提供了对应的Builder,例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder,开发者可以通过这些Builder来创建对应的流。
独立使用Stream的步骤如下:
- 使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。
- 重复调用Builder的add()方法向该流中添加多个元素。
- 调用Builder的build()方法获取对应的Stream。
- 调用Stream的聚集方法。
简述Java中的IO流
IO用于表示数据的输入输出,Java把不同的输入/输出源抽象表示为流(Stream)。流是从起源到接收的有序数据,有了它程序可以采用同一方式访问不同的输入/输出源。
- 以File开头的文件流用于访问文件;
- 以ByteArray/CharArray开头的流用于访问内存中的数组;
- 以Piped开头的管道流用于访问管道,实现进程之间的通信;
- 以String开头的流用于访问内存中的字符串;
- 以Buffered开头的缓冲流,用于在读写数据时对数据进行缓存,以减少IO次数;
- InputStreamReader、InputStreamWriter是转换流,用于将字节流转换为字符流;
- 以Object开头的流是对象流,用于实现对象的序列化;
- 以Print开头的流是打印流,用于简化打印操作;
- 以Pushback开头的流是推回输入流,用于将已读入的数据推回到缓冲区,从而实现再次读取;
- 以Data开头的流是特殊流,用于读写Java基本类型的数据。
怎么用流打开一个大文件?
为了避免直接将文件中的数据都读入内存中,可以采用多次读取的方式
- 采用缓冲流,通过与缓冲流的交互,减少与设备的交互次数。每一次读取一批数据是将缓冲区存满,每次调用读取方法不是从设备而是从缓冲区进行读取。
- 使用NIO。NIO采用内存映射的文件来处理输入/输出,NIO将文件或文件的一段区域映射到内存上,可以通过像通过访问内存的方式来访问文件。
NIO的实现原理
NIO主要由三个核心部分组成:Channel、Buffer、Selector
线程-》Selector-》多个Channel<=>(互相写入)Buffer(capacity,position,limit)
Java的序列化与反序列化
序列化机制可以将对象转换成字节序列,这些字节序列可以保存到磁盘上,也可以在网络上传输,对象的序列化是指将一个Java对象写入IO流中,对象的反序列化是指从IO流中回复该对象。
实现序列化:对象的类需要实现Serializable接口,使用ObjectInputStream对象的writeObject()方法和ObjectOutputStream的readObject()方法。
Serializable接口为什么需要定义serialVersionUID变量?
代表了序列化的版本,在操作之前需要确定版本是否一致。
除了Java自带的序列化之外,还了解哪些序列化工具?
JSON:目前使用频繁的格式化数据工具,简单直观,可读性好,有fastjson
protohuf、Thrift、Avro
并发
什么是线程安全?
当多个线程访问某个类,不管运行时环境采用了何种调度方式或者这些线程如何交替执行,并且在不需要额外的同步或者协同,这些类都能表现出正确的行为,那么这个类就是线程安全的。
保证线程安全的策略?
- 使用不可变的:像String、Integer、Long这些都是final类型的类,任何一个线程都改变不了他们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
- 加锁和CAS:我们最常使用的保证线程安全的手段,使用synchronized关键字,使用显示锁,使用各种原子变量,修改数据时使用CAS机制等
如何在两个进程间共享数据
通过使用共享变量
创建线程的方式有几种?
三种方式
- 继承Thread类
- 定义Thread类的子类,重写run()方法,将该方法作为线程执行体
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的strat()方法即启动了该线程
- 实现Runnable接口
- 定义Runnable接口的实现类,实现该接口的run()方法,将该方法作为线程执行体
- 创建Runnable实现类的实例,并将其作为Thread的target来创建thread对象
- 调用该对象的start()方法启动该线程
- 实现Callable接口
- 创建Callable接口的实现类,并实现call()方法,该call()方法作为线程执行体,且该call()方法有返回值,在创建Callable实现类的实例
- 使用Future Task类来包装Callable对象,该Future Task对象封装了该Callable对象的call()方法的返回值
- 使用Future Task对象作为Thread对象的target创建并启动新线程
- 调用Future Task对象的get()方法获得子线程执行结束后的返回值</