java面试基础常问总结
2022-06-07 15:20:53 1 举报
AI智能生成
java常见面试常问总结,本人遇到常见的基础问题,可以用于个人总结查看..
作者其他创作
大纲/内容
集合框架
分为2种
collection
set
list
map
ArrayList与linkeList与vector比较
ArrayList
实现是一个可变数组,默认长度是10。但是没有设置容量默认的是空数组,只有第一步add的时候会进行扩容至10(重新创建了数组),后续扩容按1.5的大小进行扩容,是线程不安全的,适用于多读取,少插入的场景。因为会根据脚标进行快速定位读取。
linkedList
是基于双向链表的实现,使用尾插法的方式,内部维护了链表长度,以及头节点和尾节点,所以获取长度要遍历。适合频繁插入/删除的场景
vector
是线程安全的,实现与ArrayList相似,也是基于数组,但是方法上都使用了synchronized关键词修饰。其扩容是原来的2倍
线程安全
Vector一定线程安全么?
Vector的每个方法都是线程安全的,但是无法保证复合操作线程安全,这种情况只能通过加锁的方式来实现。但是同步容易对其所以方法都加了锁,这就导致了多个线程访问同一个容器只能顺序访问,降低了容器并发能力。
针对同步容器的复合操作,一般都发生在Map,所以在ConcurrentHashMap中增加了对常用符合操作的支持,比如putlfAbsent()等可以保证线程安全
CopyOnWriteArrayList
写时加锁,使用了一种叫写时复制的方法;读操作是可以不用加锁的
hashmap、hashTable、ConcurrentHashMap比较
hashMap
是map类型常用的数据结构,其底部实现1.7前是数组+链表,1.8后数组+链表/红黑树。它key可以null,hash默认为0,扩容以2的幂等次。是线程不安全。
hashTable
实现形式与hashMap差不多。是线程安全的,是继承了Dictionary,但是全程加锁,性能不好。也是key-value的模式,但key不能null
ConcurrentHashMap
是JUC并发包的一种,在hashMap基础上做了修改,比hashTable性能好,所以采用了分段的思想,把原本的一个数组分为16段,可以最多容纳16个线程并发操作,16个段叫segment,是基于ReetrantLock来实现的。
hashMap详解
说下HashMap
底层结构
它1.8之前是数组+链表,1.8后数组+链表/红黑树的数据结构,主体还是数组(Java7叫Entry在Java8中叫Node),数组每个地方都存有key-value这样的实例,当put一个值时,会通过哈希函数计算出插入的位置,再进行保存。而链表的存在是为了解决哈希冲突,因为哈希本身就存在概率性,再好的哈希函数都可能会出现哈希冲突,所以当出现哈希冲突到同一个存储地址值上,就形成了链表,index指向下一个节点,形成一个单链表。1.8之后当链表长度达到8会变成红黑树
红黑树?
是一种自平衡二叉查找树,是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
Entry节点如何插入链表
1.8前是头插法,新来的值会取代原来的值,原来的值会顺推到链表中去,作者认为后来的值被查找性更大,提高效率
1.8之后都是尾插法,1、为了解决并发情况下出现的闭环造成死循环
扩容原理resize
负载因子默认为0.75
负载因子太小容易触发扩容,如果太大容易出现碰撞。
就比如当我们容量为100,当存入第76个判断发现需要进行resize,就需要进行扩容
1.7扩容步骤
扩容,创建一个新的Entry空数组,长度为原来两倍
ReHash:遍历原来的数组,把所以的Entry重新Hash到新数组
1.8扩容
先插入数据,然后进行扩容,hash不是线程安全,在扩容期间能够读到扩容的值。
为什么重新Hash?
重新Hash是因为长度扩大后,hash规则也随之改变
存储原理
hashMap为什么线程不安全
会出现的问题
1.7先并发情况下会出现死循环或者数据丢失问题
1.8并发情况下会出现数据覆盖问题
死循环的主要问题
在对table进行扩容到newTable后,会将数据转移到newTable中,转移使用的是头插法,也就是链表的顺序会翻转,这里是形成死循环的关键点
hashMap如何解决hash冲突
当两个不同元素,一个元素通过哈希函数得出的实际存储地址进行插入时,发现已经被另外一个函数占用,这就是所谓的哈希冲突
解决方式:1、开放定址法(发生冲突,找下一块未被占用的存储地址)。2、链地址发,hashMap使用的,也就是数组+链表的方式
hashMap扩容为什么是2的次幂
重写equals需要重写hashCode方法
ConcurrentHashMap详解
与HashMap区别
底层原理
数据结构
是线程安全的map结构,它的核心思想是分段锁。在1.7的时候,内部维护了segment数组,默认是16个,可以同时16个线程操作,segment继承了reentrantlock使用了互斥锁。而在1.8做了大改动,废除了segment,采用了cas +synchronize方式进行分段锁(还有自旋锁保证),而且节点对象也由HashEntiry改为Node。node支持链表和红黑树的转换。
扩容方式
1.7
1.7是基于segment的,segment内部维护了HashEntiry数组,扩容与hashmap一致
1.8
利用ForwardingNode首先会根据机器内核来分配每个线程分到的busket数,这样可以做到多线程协助迁移,提升效率
然后根据自己分配的busket数来进行节点转移,如果为空就放置ForwardingNode,代表已经迁移完成代表已经迁移完成
如果是非空节点,加锁,链路循环,进行迁移
1.8put过程
先计算出hash值,根据hash值选出Node数组下标,(默认数组是空的,所以一开始put的时候会初始化,指定负载因子0.75,不可变
接着判断是否为空,如果空,则用cas的操作来赋值首节点,如果失败,则因为自旋,会进入非空节点的逻辑。
这时候会用synchronize加锁头节点(保证整条链路锁定)
这时候还会进行二次判断,是否同一个首节点,在分首节点到底是链表还是树结构,进行遍历判断
如何保证线程安全
volatile
特性是什么
并发度高的原因
子主题
Collections.synchronizedMap
TreeMap
一致性哈希
是什么?
一致性哈希算法是一种特殊的哈希算法,目的是解决分布式缓存的问题。
在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。
一致性哈希解决了简单哈希算法在分布式哈希表(Distributed Hash Table,DHT)中存在的动态伸缩等问题。
一致性哈希解决了简单哈希算法在分布式哈希表(Distributed Hash Table,DHT)中存在的动态伸缩等问题。
网络
TCP与UDP
TCP
TCP向上层提供面向连接的可靠服务,面向连接、可靠、字节流
传输效率慢、所需资源多
实现协议:HTTP、FTP、SMTP
UDP
提供无连接不可靠服,无连接、不可靠、数据报文段
传输效率快、所需资源少
实现协议:RIP、DNS、SNMP
常用场景
TCP
文件传输(FTP HTTP 对数据准确性要求高,速度可以相对慢)
发送或接收邮件(POP IMAP SMTP 对数据准确性要求高,非紧急应用)
远程登录(TELNET SSH 对数据准确性有一定要求,有连接的概念)
UDP
即时通信(QQ聊天 对数据准确性和丢包要求比较低,但速度必须快)
在线视频(RTSP 速度一定要快,保证视频连续,但是偶尔花了一个图像帧,人们还是能接受的)
网络语音电话(VoIP 语音数据包一般比较小,需要高速发送,偶尔断音或串音也没有问题)
使用
java的TCP网络编程
TCP三次握手
过程
第一次握手:客户端发送syn包到服务器,并且进入syn_sent状态,等待服务确认状态
第二次握手:服务器接受到syn包并确认,同时发送一个syn包+ack包,此时服务器进入syn_recv状态,等待接收状态
第三次握手:客户端接收到服务器的syn_ack包,向服务器发送确认包ack,此包发送完毕,客户端和服务端进入已确认状态,可以进行传输
为什么是三次握手
主要是为了建立可靠的通信信道,保证客户端与服务端同时具备发送、接收数据的能力
TCP四次挥手
过程
客户端——发送带有FIN标志的数据包——服务端,关闭与服务端的连接 ,客户端进入FIN-WAIT-1状态
服务端收到这个 FIN,它发回⼀ 个 ACK,确认序号为收到的序号加1,服务端就进入了CLOSE-WAIT状态
服务端——发送⼀个FIN数据包——客户端,关闭与客户端的连接,客户端就进入FIN-WAIT-2状态
客户端收到这个 FIN,发回 ACK 报⽂确认,并将确认序号设置为收到序号加1,TIME-WAIT状态
为什么四次
因为需要确保客户端与服务端的数据能够完成传输。
输入URL获取网页过程
输入网址,客户端去dns找域名对应的ip地址,拿到服务端的ip会进行三次握手,连接成功传输数据,服务器处理请求返回响应数据渲染到页面
Http
常见状态码
200
请求成功
301
永久重定向
302
暂时重定向
400
客户端请求语法错误,服务端无法理解
401
未授权
403
服务端拒绝执行此访问(跨域)
404
未找到资源
500
服务器错误
502
网关错误
503
服务端未启动或未找到
请求
GET
向特定资源发送请求,查询数据,并返回实体
POST
向指定资源提交数据进行处理请求,可能会导致新的资源建立、已有资源修改
PUT
向服务器上传新的内容
HEDA
类似GET请求,返回的响应中没有具体的内容,用于获取报头
DELETE
请求服务器删除指定标识的资源
OPTIONS
可以用来向服务器发送请求来测试服务器的功能性
TRACE
回显服务器收到的请求,用于测试或诊断
CONNECT
HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器
子主题
Nginx
Nginx如何禁止国外ip访问网站?
1、Nginx的ngx_http_geoip2 模块方法
操作具体步骤:https://mp.weixin.qq.com/s/1M4n7G_KTUOGXfPqtHVU3w
其实就是先给Nginx1.18以上的版本安装这个模块,可以下载国外的ip库,放到服务器,接着根据配置文件访问判断访问的ip是否是国外的,如果是直接返回404即可
2、Nginx的防火墙工具也可以
3、服务器应该可以直接设置可以访问IP区域
多线程
Synchronized
对Synchronized的理解
特性保证
有序性
as-if-serial
happens-before
可见性
内存强制制新
原子性
单一线程持有
可重入性
计数器
synchronized与Lock区别
sync是关键字,是jvm层面的底层啥都帮我们做了,而Lock是一个接口,是jdk层面的有丰富的api
sync会自动释放锁,而Lock必须手动释放锁
sync是不可中断的,Lock可以中断也可以不中断、
通过Lock可以知道线程是否拿到锁,sync不能
sync能够锁住方法,代码块,而Lock只能锁住代码块
sync是非公平锁,ReentrantLock可以控制是否是公平锁,默认是非公平锁
sync劣势是锁不可逆
在项目中怎么使用?
最主要三种方式
修饰实例方法
修饰静态方法
修饰代码块
总结
Synchronized关键字底层原理
1.6之后做了哪些优化
与ReentrantLock区别
volatile
说说java内存模型
理解
主要是为了解决线程的可见性和有序性问题。
可见性与重排序
可见性就是说一个线程修改了共享变量,但是另外一个线程却看不见。
重排序就是比如一个对象实例化步骤是【分配内存空间-初始化-设置内存地址】,操作系统可以对指令进行重排序,顺序会改变,我们可以通过给变量添加volatile。
与Synchonized区别
总结
volatile修饰符适合场景:某个属性被多个线程共享,当一个线程修改值后其他线程能够拿到最新的值,或者作为触发器实现轻量级同步。、
volatile属性的读写操作都是无锁的,不能代替synchonized,因为本身没有提供原子性和互斥性。因为没有锁,所有不需要获取锁和释放锁上来费时间,它是低成本的。
volatile只能用在属性,这样compolers就不会对这属性做指令重排序
volatile提供了可见性,任何线程对其修改立马对其他线程可见,volatile属性不会被线程缓存,实在会从主存中读取
ThreadLocal
简介
ThreadLocal的作用主要是数据隔离,填充的数据只属于当前线程,变量对于其他线程而言是相对隔离的。
使用
场景
Spring实现事物隔离,使用Threadlocal的方式来保证单个线程中的数据库操作使用的是同一个数据库连接,同时采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之前的切换。
很多场景下的cookie、session等数据隔离都是通过ThreadLocal去做实现的。
在我们的系统里面我们常常使用了ThreadLocal类型参数进行存储租户信息,对于线程进行了数据隔离
原理
使用
初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove()之前去get,都可以拿到之前set的值。做到了线程隔离,所以其他线程无法拿到其他的值。
源码
每个线程Thread都维护了自己的threadLocals变量,所以每个线程创建ThreadLocal的时候,实际上数据都是存在自己的线程Thread的threadLocals变量里面的,别人没有办法拿到,从而实现了隔离。
在java中,栈内存是每个线程都存在的,栈内存可以理解为每个线程的私有空间,其存储的变量只属于自己线程。但是Threandlocal并不是将值放在栈内存中,ThreadLocal的值其实是被线程实例持有,它们都是位于堆上的,只是通过一些技巧将其修改成了线程可见,我记得是通过复制出n份数据出来给每个线程一份来实现。
ThreadLocal子类及原理
InheritableThredLocal
继承了ThreadLocal,并重写了chilValue、getMap、createMap,对该类的操作实际是对线程ThreadLocalMap的操作。子线程能够读取父线程的数据,实际原因是新建子线程的时候会从父线程copy数据
内存泄漏问题
发生原因
在ThreadLocal没有外部强引用的时候,发生GC会被回收,如果创建ThreadLocal的线程一直处于持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄漏。垃圾回收优先级很低得线程,因此不一定很快发现弱引用的对象。
解决办法
在代码最后使用remove()清空就好了。
线程池
为什么需要线程池
降低系统消耗
重复利用已经创建的线程降低线程创建和销毁造成的资源消耗
提高响应速度
当任务到达时,任务不需要等到线程创建就可以立即执行
提供线程可以管理性
可以通过设置何立分配、调优、监控
线程池工作流程
判断核心线程池是否都有执行任务,否-》创建一个新的工作线程执行任务。是-》走下一个流程
判断工作队列是否已满,否-》新任务存储再这个工作队列里,是-》走下一个流程
判断线程池里的线程是否都在工作状态,否-》创建一个新的工作线程来执行任务。是-》只想下一个流程
按照设置的策略来处理无法执行的任务
线程池参数有哪些,作用?
corePoolSize
核心线程池大小
maximumPoolSize
线程池最大数
keepAliceTime
线程保持活动时间
unit
keepAliceTime线程保持活动时间单位
workQueue
任务队列
threadFactory
设置创建线程的工厂
handler
饱和策略也叫拒绝策略
执行execute()与submit()方法的区别
这两种方式都是向线程池提交任务
execute()无返回值,所以无法判断任务是否被执行成功
submit()用于提交需要有返回值的任务
线程池大小设置
cpu密集型
该任务需要大量的运算时候,没有阻塞,需要高速运算,这时需要的线程数较少,可以设置cup数+1
IO密集型
当任务不需要一直运算的时候,可以多分配一些线程数,如cup数*2
关闭线程
shutdown()
shutdownNow()
如何创建线程池
线程池原理
ThreadPoolExecutor分析
Atomic
AQS
介绍
原理分析
原理
AQS对资源的共享方式
AQS底层使用了哦版方法模式
组件总结
线程安全
什么是线程安全
线程安全并非说说的是保证线程的安全,真正的说法是保证内存中资源的安全。现在系统普遍都是支持多进程同时运行,每个进程都会被操作系统分配相应的内存空间,每个进程中的内存不可相互干扰,这是操作系统保证的,进程分配的内存中有一块特殊的存在,我们叫它叫做堆,他是公共的区域,进程中的任何线程都能够进行访问该区域,这就是造成问题的原因。所以线程安全值的是堆内存中的数据可以被任何线程访问,在没有限制的情况下会被意外修改的风险。
解决方案
放在栈内存空间,其他线程无权访问
在程序中操作系统会给每个线程分配属于它自己的内存空间,称为栈内存
TheadLocal
使用TheanLocal创建的变量,每个线程在运行时都会拷贝一份存储在自己本地。这些ThreadLocal数据是属于Thread类的成员变量级别的,从所在“位置”的角度来讲,这些ThreadLocal数据是分配在公共区域的堆内存中的。就是把堆内存中的一个数据复制成N份,每个线程认领一份。
final
在参数添加final,这样所以线程都动不了这个参数。只能读不能修改
(互斥)锁
如果公共区域(堆内存)的数据,要被多个线程操作时,确保数据的安全性或一致性,需要在数据旁边加一把锁,要想操作数据,需要先获取一把锁。
CAS
因为锁的获取和释放是要花费一定的代价,如果在线程数目特别少的时候,可能根本就不会有别的线程来操作数据,此时你还要获取锁和释放锁,可以说就是一种浪费。
线程5大状态
新建状态
新建线程对象,没有调用start()方法前
就绪状态
抵用start方法后进入就绪状态,线程再睡眠和挂起中恢复的时候都会进入就绪状态
运行状态
线程被设置为当前线程,开始执行run()方法,就是线程进入运行状态
阻塞状态
线程被暂停,比如说调用sleep()后线程进入阻塞状态
阻塞超时状态
死亡状态
线程执行结束
锁
互斥锁
最底层的两种就是会【互斥锁与自旋锁】,很多高级锁都是基于它们实现的。
加锁失败后线程会释放cpu,给其他线程,失败线程而出现阻塞的现象,由操作系统内核实现的,内核会将线程置为睡眠状态,等到锁被释放后,内核在核实时机唤醒线程,当这个线程获得锁后继续执行。
所以加锁失败会从用户态陷入到内核态,让内核帮我们切换线程,存在一定的性能开销成本。开销成本就是两次线程上下文切换的成本,从运行状态到睡眠,再从睡眠到就绪。
所以当锁住的代码时间比较短,那可能上下文切换的时间比你锁住的代码块执行时间还要快。如果这样就不应该用互斥锁,而应该用自旋锁。
自旋锁
枷锁失败后忙等待,直到拿到锁.自旋锁是比较简单的锁,一直自旋,利用cpu周期,直到锁可用。是通过cpu提供的cas函数,在【用户态】完成加锁和解锁操作,不会主动产生线程上下文切换,所以相对互斥锁来说,会快一些开销小一些。加锁有两步:一,查看锁状态,如果锁空闲,则执行第二部,二将锁设置为当前线程持有;
自选时间和被锁住的代码执行时间成正比关系
读写锁
读写锁适用于能够明确区分读操作和写操作的场景。
子主题
乐观锁
它认为冲突概率很低,先修改资源,再认证这个段时间有没有发生过冲突,如果没有被修改过,操作完成,如果发现已经修改过了这个资源,就放弃本次操作。
悲观锁
多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前要先上锁
公平锁/非公平锁
公平锁
是指多个线程申请锁是按照顺序来获取锁的
非公平锁
是指不按照顺序申请来获取锁的,会造成优先级反转或者饥饿现象
可重入锁(递归锁)
是指再同一个线程再外层方法获取锁的时候,再进入内层方法会自动获取锁。好处是可一定程度避免死锁
独享所/共享锁
独享锁
只该锁只能一次只能一个线程所持有
共享锁
只锁可被多个线程所持有
分段锁
其实是一种锁的设计,并不是一种具体的锁,对于ConcurrentHashMap其并发的实现就是通过分段锁的形式来实现高效的并发操作的。
子主题
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized,再java5通过引入锁升级的机制来实现Synchronized。这三种锁的状态是通过对象监视器再对象头中的阻断来表明的。
偏向锁是指一段同步代码一直被一个线程锁访问,那么该线程会自动获取锁,降低了获取锁的代价
轻量级锁是指当锁是偏量级锁的时候,被另外一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,请外一个线程虽然在自旋,单自旋不会一直持续下去,当自旋到一定次数的时候,还没有获取到锁就会进入到阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的锁进入阻塞,性能降低。
基础
Java常见区别知识点
重载与重写
重载指在同一个类中定义多个方法,这些方法名称相同,签名不同
重写指在子类中的方法的名称和签名都和父类相同,使用override注解
序列化与反序列化
序列化是一种用来处理对象流的机制,将对象转化成二进制流,可以将对象持久化或者网络传输
实现序列化通过实现Serializable即可
反序列化就是将二进制流转换成对象过程
优点
实现数据持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里)Redis的RDB
也可以利用序列化实现远程通信,即在网络上传送对象的字节序列。 Google的protoBuf
常见的序列化方式
JDK原生
实现序列化通过实现Serializable即可
JSON
ProtoBuf
Hession
Kryo 等
如何选择序列化方式
考虑三个方面
协议是否支持跨平台
序列化速度
序列化生成的体积
接口与抽象类区别
接口方法默认都是抽象方法,而抽象类里有抽象方法也有非抽象方法
一个类可以继承多个接口,但只能实现一个抽象类
jdk8以后添加了default修饰符修饰方法,接口可以写实体。抽象类不可以有default方法
自动装箱与拆箱
前提
java里面万事万物都是对象,但是有例外就是8个基本数据类型不是对象,因此会很不方便,所以对基本数据类型提供了对应的包装类
理解
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
成员变量与局部变量
定义的位置不一样
成员变量在类体内部,方法体外面
局部变量在方法体内部
局部变量在方法体内部
作用范围不一样
成员变量的作用范围是整个类
局部变量的作用范围是方法体
局部变量的作用范围是方法体
默认值不一样
成员变量有默认值
局部变量没有默认值
局部变量没有默认值
内存的位置不一样
成员变量:位于堆内存
局部变量:位于栈内存
局部变量:位于栈内存
生命周期不一样
局部变量:随着方法进栈而诞生,出栈而消亡
成员变量:睡着对象创建而诞生,GC回收而消亡
成员变量:睡着对象创建而诞生,GC回收而消亡
静态方法与实例方法
区别
在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
静态方法不能访问实例成员
因为实例对象是属于某个对象,而静态方法在执行的时候不一定有对象,所有不能使用实例对象,也不能使用this
其实真的原因是:JVM加载类的时,就会执行该static,静态优先于其他产生对象产生,就是satic调用非静态时并未产生对象,所有this不代表任何对象为null,未进行初始化操作
静态有利有弊
利:对对象的共享数据进行单独空间的存储,节省空间,没有必要每一格对象中都存储一份。可以直接被类名调用。
弊:生命周期过长,访问出现局限性(静态虽好,只能访问静态)
什么时候使用静态
如果你正在编写实用程序类,则不应更改它们。
如果该方法未使用任何实例变量。
如果有任何操作不依赖于实例创建。
如果某些代码可以被所有实例方法轻松共享,则将该代码提取到静态方法中。
如果你确定该方法的定义永远不会更改或覆盖。由于静态方法不能被覆盖。
如果该方法未使用任何实例变量。
如果有任何操作不依赖于实例创建。
如果某些代码可以被所有实例方法轻松共享,则将该代码提取到静态方法中。
如果你确定该方法的定义永远不会更改或覆盖。由于静态方法不能被覆盖。
1、什么时候使用静态的成员属性:
当属于同一个类的所有对象出现共享数据时,需要将存储这个共享数据的成员变量用static修饰
2、什么时候使用静态的成员方法:
当功能内部没有访问到非静态的成员时(对象特有的数据),那么该功能可以定义成静态的。
当属于同一个类的所有对象出现共享数据时,需要将存储这个共享数据的成员变量用static修饰
2、什么时候使用静态的成员方法:
当功能内部没有访问到非静态的成员时(对象特有的数据),那么该功能可以定义成静态的。
==与equal与hashCode
区别
==
它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
equals
情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
hashCode
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。
为什么重写 equals 时必须重写 hashCode 方法?
如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?
因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode。
我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
final与finally与finalize
final
final可以用来修饰类,方法和变量(成员变量或局部变量)
修饰方法,把方法锁定,以防止继承类对其进行更改。
当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的
finally
作为异常的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下。
finally特殊情况也不一定执行:如对应的try语句块正常执行finally语块才执行。try中执行System.exit(0)语句,终止了虚拟机运行也不会运行
finalize
Object下的一个方法,这个方法在gc启动,该对象被回收的时候被调用。其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。
特殊情况下,需要程序员实现finalize,当对象被回收的时候释放一些资源,比如:一个socket链接,在对象初始化时创建,整个生命周期内有效,那么就需要实现finalize,关闭这个链接。
&与&&
两个都是逻辑与,当两遍都为true时,返回会true。
&&有短路功能,第一个判断为false,后面就不判断了。&会进行第二个判断
int与Integer
区别
int是java基本数据类型,Integer是int的包装类
Integer变量必须实例化之后才能使用,而int变量不需要实例化
Integer实际是对象的引用,生成一个指针指向对象,而int直接存数值
Integer默认值是null,int默认值是0
常见判断
new两个Integer对象判断地址结构都是false
int i = new Integer(100); 与 Integer j = new Integer(100); j会自动拆箱,i与j进行值的比较,返回false
Integer i = 100;与 Integer j = new Integer(100);i指向的是java常量池中的对象,而j new出来的对象指向堆新建对象,两个地址不一致返回false
Integer i = 100;与 Integer j = 100;当值在-127~128之前会进行拆箱就是值进行比较
值的传递与引用
传递
方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy,此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值。
引用
也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址;
在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象。
在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象。
结论
(1)基本数据类型传值,对形参的修改不会影响实参;
(2)引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象;
(3)String, Integer, Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会修改实参对象。
(2)引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象;
(3)String, Integer, Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会修改实参对象。
弱引用与强引用
理解
对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
区别
强引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps:强引用其实也就是我们平时A a = new A()这个意思。
弱引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
软引用
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
浅拷贝与深拷贝
理解
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
private、public、protect
private是可以声明私有的成员,只能本类的成员进行访问,外部类、子类不能访问、继承
public是可以声明公共的成员,可以被本类的成员调用,也能外部类、其他作用域的类调用
protect是声明受保护的成员,可以被本类和子类调用,但是子类不能对方法进行重写
默认不写:只有本类和和同一个包下面的类可以调用
public是可以声明公共的成员,可以被本类的成员调用,也能外部类、其他作用域的类调用
protect是声明受保护的成员,可以被本类和子类调用,但是子类不能对方法进行重写
默认不写:只有本类和和同一个包下面的类可以调用
面向对象和面向过程
过程
面向过程性能比面向对象高,因为类阿紫调用时需要实例化,开销比较大,比较消耗资源,所以一般性能是重要的考量因素的时候,比如单片机,嵌入式开发、liunx等都采用的是面向过程开发。但是面向过程没有面向对象易于维护、复用、扩展
对象
面向对象易于维护、易于复用、易于扩展
内存溢出和内存泄漏
内存溢出
没有足够的内存可以使用
内存中加载的数据量过于庞大,如一次从数据库中取出过多数据
集合类中有对对象的引用,使用完后未清理,使得jVM不能回收
代码中存在死循环或循环产生或多重复的对象实体
启动参数内存值设定得过小
内存泄漏
强引用对象没有释放使用的内存空间,长时间的堆积造成
过滤器与拦截器与监听器
区别
实现方式不同:
过滤器的实现基于回调函数。拦截器基于Java的反射机制【动态代理】实现。
根本原因,实现方式
Filter基于回调函数,我们需要实现的filter接口中doFilter方法就是回调函数,而interceptor则基于
java本身的反射机制,这是两者最本质的区别。
java本身的反射机制,这是两者最本质的区别。
触发时机不同:
过滤器:对请求在进入后Servlet之前或之后进行处理。
拦截器:对请求在handler【Controller】前后进行处理。
拦截器:对请求在handler【Controller】前后进行处理。
作用域
拦截器只能对action请求起作用,过滤器可以对几乎所有的请求起作用
过滤器(Filter)
所谓过滤器顾名思义是用来过滤的,Java的过滤器能够为我们提供系统级别的过滤,也就是说,能过滤所有的web请求,这一点,是拦截器无法做到的。在Java Web中,你传入request,response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者struts的action进行业务逻辑,比如过滤掉非法url(不是login.do的地址请求,如果用户没有登陆都过滤掉),或者在传入servlet或者struts的action前统一设置字符集,或者去除掉一些非法字符(聊天室经常用到的,一些骂人的话)。filter 流程是线性的,url传来之后,检查之后,可保持原来的流程继续向下执行,被下一个filter, servlet接收。
拦截器(Intercepptor)
ava里的拦截器提供的是非系统级别的拦截,也就是说,就覆盖面来说,拦截器不如过滤器强大,但是更有针对性。Java中的拦截器是基于Java反射机制实现的,更准确的划分,应该是基于JDK实现的动态代理。它依赖于具体的接口,在运行期间动态生成字节码。拦截器是动态拦截Action调用的对象,它提供了一种机制可以使开发者在一个Action执行的前后执行一段代码,也可以在一个Action执行前阻止其执行,同时也提供了一种可以提取Action中可重用部分代码的方式。在AOP中,拦截器用于在某个方法或者字段被访问之前,进行拦截然后再之前或者之后加入某些操作。java的拦截器主要是用在插件上,扩展件上比如 Hibernate Spring Struts2等,有点类似面向切片的技术,在用之前先要在配置文件即xml,文件里声明一段的那个东西。
监听器(Listener)
Java的监听器,也是系统级别的监听。监听器随web应用的启动而启动。Java的监听器在c/s模式里面经常用到,它会对特定的事件产生产生一个处理。监听在很多模式下用到,比如说观察者模式,就是一个使用监听器来实现的,在比如统计网站的在线人数。又比如struts2可以用监听来启动。Servlet监听器用于监听一些重要事件的发生,监听器对象可以在事情发生前、发生后可以做一些必要的处理。
幂等与去重
区别
去重:是对请求或者消息一定时间内多次请求去重
幂等:则是保证请求或者消息在任意时间内进行处理,都需要保证他的结果一致性
去重:
子主题
幂等:
转发与重定向
Session与Cookie
他们都是用于跟踪用户会话的方式
Cookie数据保存在客户端,Session数据保存在服务端
Cookie不安全,Session较安全
Cookie将用户信息保存在浏览器,Seeion用于通过服务端记录用户状态
Java常见知识点
反射
why
是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且都能够调用它的任意一个方法;
反射原理
反射首先是能够获取到Java中的反射类的字节码,然后将字节码中的方法,变量,构造函数等映射成 相应的 Method、Filed、Constructor 等类
如何得到Class的实例
1.类名.class(就是一份字节码)
2.Class.forName(String className);根据一个类的全限定名来构建Class对象
3.每一个对象多有getClass()方法:obj.getClass();返回对象的真实类型
2.Class.forName(String className);根据一个类的全限定名来构建Class对象
3.每一个对象多有getClass()方法:obj.getClass();返回对象的真实类型
使用场景
开发通用框架
反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,需要根据配置文件运行时动态加载不同的对象或类,调用不同的方法。
动态代理
在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反射技术来实现了。
JDK
spring默认动态代理,需要实现接口
自定义注解
注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。
构造函数
构造方法可以被重载,只有当类中没有显性声明任何构造方法时,才会有默认构造方法。
构造方法没有返回值,构造方法的作用是创建新对象
构造方法没有返回值,构造方法的作用是创建新对象
面向对象
三个特性
封装
对抽象的事物抽象化成一个对象,并对其对象的属性私有化,同时提供一些能被外界访问属性的方法;
继承
子类扩展新的数据域或功能,并复用父类的属性与功能,单继承,多实现;
多态
通过继承(多个⼦类对同⼀⽅法的重写)、也可以通过接⼝(实现接⼝并覆盖接⼝)
异常
异常类
父类throwable
error
exception
RuntimeException
运行时异常
空指针、下标越界
Checkexception
受检查异常
sql异常,io异常,NotFountException
常见异常
算术异常
类转换异常
非法参数异常
下表越界异常
空指针异常
安全异常
泛型
?与T
?可以表示成占位符,它自己也不知道list集合中会存放多少种类型的数据,所以这样就表明我们的list中存放N种数据类型也是可以的。
泛型与泛型擦除
泛型的本质是参数化类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
Java的泛型是伪泛型,使用泛型的时候加上类型参数,在编译器编译生成的字节码的时候会去掉,这个过程成为类型擦除。
如List<String>等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。
可以通过反射添加其他类型元素
this
关键字 `this` 代表当前对象的引用。当前对象指的是调用类中的属性或方法的对象
关键字 `this` 不可以在静态方法中使用。静态方法不依赖于类的具体对象的引用
super
可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
元注解
java基础难点
类加载器
理解
java类加载器负责将编译好的class 文件加载到 Java 虚拟机(JVM)中的运行时数据区中,供执行引擎调用
双亲委派模型
spi机制
是什么
是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
为什么要有SPI
系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
它提供了机制
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。
常用场景
数据库驱动加载接口实现类的加载,JDBC加载不同类型数据库的驱动
SLF4J加载不同提供商的日志实现类
Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口
使用介绍
1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
2、接口实现类所在的jar包放在主程序的classpath中;
3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
4、SPI的实现类必须携带一个不带参数的构造方法;
2、接口实现类所在的jar包放在主程序的classpath中;
3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
4、SPI的实现类必须携带一个不带参数的构造方法;
常用数据类型
基础数据类型
byte、short、int、long、float、double、boolean、char
(1个字节是8个bit) 整数型:byte(1字节)、short(2字节)、int(4字节)、long(8字节) 浮点型:float(4字节)、double(8字节) 布尔型:boolean(1字节) 字符型:char(2字节)
String
StringBuilder与StringBuffer
区别
StringBuffet 线程安全
很多方法添加了Synchronized修饰符,保证线程安全,所以效率会比较慢
StringBuilder更快
比StringBuffet与String都快,一个因为加了锁,一个因为重新赋值的时候是重新创建了一个新的对象
StringBuilder底层
底层是不是一个Stirng,而是数组,当值变动的时候可以通过数组扩容方式快速变动。
继承了AbstracatStirngBuilder,初始化默认数组长度为16,扩容系数为value.length * 2 + 2
常用方法
String创建对象
String str =“abc”过程
首先在常量池中查找是否存在内容为“abc”的字符串对象
如果不存在则在常量池中创建"abc"对象,并str 引用此对象
如果常量池中存在对象则直接让str 引用存在的对象
String str = new String("abc") 过程
首先在堆中(不是常量池)创建一个指定对象,并让str引用指向这个对象
在字符串常量池中查找是否存在内容为"abc"字符串对象
若存在则将此对象与str进行引用关联(即让那个特殊的成员变量value的指针指向它)
若不存在则在字符串常量池中创建内容为"abc"的字符串对象,并将此对象与str进行关联。
str存在栈中,new出来的字符串对象存在堆中,abc存在字符串常量池中
常见问题
String底层原理?
String如何保证不变?
使用+连接符拼接String字符串的原理是什么?
用常量对String赋值和对String new的区别?
字符串常量池在那里
Object有几种方法
java类都有一个共同祖先,就是Object类,一个类没有extends明确指出继承某个类,那默认就是继承了Object
一共13个方法
Object()
构造方法
registerNatives
为是JVM发现本机功能,他们被一定的方式命名。
clone()
用途是用来另存一个当前存在的对象,只有实现了Cloneable接口才可以调用该方法
getClass()
final方法,用于获取运行时的类型
equals()
用来比较两个对象内容是否相等
hashCode()
该方法返回其所在对象的物理地址,常会和equals方法同时重写,确保相等两个对象拥有相等的hashCode
toString()
返回该对象的字符串表示
wait()
导致当前线程等待,直到其他线程调用此对象的notif()或notifyAll()方法,或者超过指定时间
wait(long tieout)
wait(long tieout,int nanos)
notify()
幻想再次对象监视器上的等待
notifyAll()
唤醒在此对象监视器上等待的所有线程
finalize()
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
jkd1.8新特性
Lambda表达式
java也承认了函数式编程,就是说函数也可以作为参数,也可以作为返回值,大大简化了代码
default关键字
打破接口里面只能有抽象方法,不能有任何方法的实现,接口里也可以有方法的实现
新时间日期
与1.7的CuncurrentHashMap对比
使用synchrnized代替了重入锁ReentrantLock
因为颗粒度降低,相对而言的低粒度加锁方式,synchroized并不比ReentrantLock差
基于JVM的synchronized优化空间大
在大数据量下,基于API的ReentrantLock会比基于JVM的内存压力开销更多内存
Stream
IO流
难点
多路复用
NIO与BIO
软件工程
UML
动态模型图
类图、对象图、实例联系图
系统图、功能图、项目结构图、部署图
静态模型图
流程图
时序图
状态图
收藏
收藏
0 条评论
下一页