java基础知识点
谈谈对Java的理解
特性:
- 面向对象:封装,继承,多态
- 跨平台:编写一次,到处运行(Write once, run anywhere)
- 垃圾收集:通过垃圾收集器(Garbage Collector)自动回收分配内存
开发:
- JRE:Java运行环境,包含JVM,类库等
- JDK:Java开发工具,包含JRE,编译器,诊断工具等
- 语言:支持泛型,反射,Lambda、注解等
- 类库:提供集合,并发,网络,IO/NIO等类库
Java是否是解释执行
java8是解释和编译混合模式。执行流程如下:
- 通过javac将源代码编译成.class文件(字节码 bytecode)
- 正常情况下通过JVM内嵌的解释器将字节码解释为机器码执行
- 在运行时通过JVM提供的JIT(Just In Time)编译器将热点代码编译成机器码执行
java9提供AOT(Ahead-of-Time)编译器,能够直接将代码编译成机器码执行
Exception与Error
Exception和Error都继承于Throwable类
- checked exception 编译时检查异常,必须捕获(IOException等)
- unchecked exception 运行时检查异常,根据具体情况捕获(NullPointerException、ArrayIndexOutOfBoundsException等)
- error 严重错误,不需要捕获(OutOfMemoryError等)
ClassNotFoundException与NoClassDefFoundError的区别
- ClassNotFoundException: 使用Class.forName(反射)加载类时,传递的类名无法在类路径中找到(书写错误等),就会抛出此异常
- NoClassDefFoundError: 代码中使用ClassLoader加载类或使用new来创建对象,在编译期间类存在但在运行期间却无法找到该类就会抛此异常
final、finally、finalize区别
- final
修饰类:不可被继承
修饰方法:不可重写(override)
修饰变量:原始类型不可再次复制,引用类型不可修改引用
匿名内部类访问局部变量时需要使用final,因为匿名内部类实际会copy一份局部变量,final可以防止出现数据一致性问题 - finally:Java保证重点代码一定要被执行的机制,除非在finally前执行了System.exit(1)、try中死循环、线程被杀死
- finalize:Object类的一个方法,保证对象在被垃圾收集前完成特定的资源回收。由于finalize执行时间不确定且可能造成程序死锁、拖慢垃圾收集等问题,Java9中已被废弃,使用Cleaner(基于虚引用PhantomReference)代替
强引用、软引用、弱引用、幻象引用有什么区别?具体使用场景是什么
- 强引用:常见的普通对象引用,当JVM内存空间不足,即使抛出OutOfMemoryError也不会回收引用对象
- 软引用:通过SoftReference类实现的引用,当JVM内存空间不足,才会去试图回收软引用指向的对象,常用于缓存中,如图片缓存框架中的内存缓存
- 弱引用:通过WeakReference类实现的引用,如果引用未被使用(使用时会被转成强引用),当垃圾回收器线程扫描到弱引用对象时,不管当前JVM内存空间是否足够都会被回收,常用于建立非强制性的对象映射关系,如访问时对象还在,就使用它,否则重现实例化,同样适用于缓存
- 虚引用:通过PhantomReference类实现的引用,无法用来访问对象(get方法返回null),仅提供了一种确保某个对象被finalize以后,做某些事情的机制,比如与引用队列(ReferenceQueue)联合使用,可用来跟踪对象被垃圾回收器回收的活动
String、StringBuffer、StringBuilder
- String:提供了构造和管理字符串的各种基本逻辑,使用final修饰的类,任何操作都不会改变它内部的成员,对string进行拼接、裁剪等操作会产生多个中间string
- StringBuffer:用于解决操作string产生太多中间对象而提供的类,通过内部维持一个数组来实现,数组默认长度16,使用synchronized实现线程安全
- StringBuilder:除了非线程安全,特点与StringBuffer一样
String内部优化点
- 字符串拼接:非静态字符串拼接在java8中会被编译成stringbuilder。java9中利用InvokeDynamic将字符串拼接优化与编译解耦,依靠运行时去优化
- 字符串常量池:java6把字符串常量池放在PermGen,可能导致PermGen内存溢出。java7把字符串常量池移动到了堆空间
- 字符串排重:java6开始可以通过intern()方法来显式排重。Oracle JDK 8u20之后提供G1 GC下字符串排重(默认关闭)
- 字符串native优化:在运行时通过JVM内部Intrinsic机制,运行特殊优化的native代码
- 紧凑字符串:java9中,string的实现从char数组改变为byte数组+coder(标识编码),重写字符串操作类和相关的Intrinsic,提供更小的内存占用、更快的操作速度
反射机制
赋予程序在运行时自省(introspect)的能力,通过反射可以直接操作类或者对象
作用:
- 使用反射可以直接操作对象
- 获取某个对象的类定义
- 获取类声明属性和方法
- 调用方法或者构造对象
- 运行时修改类定义
缺点:
- 性能:能不用反射实现的需求则尽可能避免使用。性能低
- 安全:反射需要运行时权限,在安全管理器(SecurityManager)下可能不能运行
- 内部接触:反射可以访问private因此可能导致副作用,破坏可移植性,打破抽象
动态代理
在运行时动态为对象构建代理,在被代理方法调用前后,添加逻辑的一种机制。常用于包装RPC调用、面向切面编程AOP等
实现动态代理方式有多种:JDK自身提供的基于反射的动态代理,ASM、cglib、javaassist等基于字节码操作机制的动态代理
区别与优势:
JDK
- 被调用者必须实现接口
- JDK本身支持可能更加可靠
- 代码实现简单。
- 平滑升级JDK,而字节码类库需要进行更新以保证可用。
- 新版本JDK也使用ASM提高性能。
类cglib
- cglib没有实现接口的限制
- 只操作我们关心的类,不需要为其他相关类增加工作量
- 基于ASM字节码,高性能。
- 通过生成业务类的子类作为代理类。
Integer
Integer是int(整形原始类型)的包装类,使用final修饰的类,包装int数据和各种基本操作类
支持自动装箱和自动拆箱(boxing/unboxing),根据上下文自动转换成原始数据类型,发生在编译阶段:装箱转换为Integer.valueOf(),拆箱转换为Integer.intValue()
支持值缓存,通过valueOf方法利用,默认缓存范围是-128到127之间,自动装箱时也生效。通过-XX:AutoBoxCacheMax=N
参数修改缓存上限
parseInt方法功能相同但无缓存机制
其他类型值缓存范围:
- Boolean,缓存了 true/false 对应实例,确切说,只会返回两个常量实例 Boolean.TRUE/FALSE
- Short,同样是缓存了 -128 到 127 之间的数值
- Byte,数值有限,所以全部都被缓存
- Character,缓存范围’\u0000’ 到 ‘\u007F’
原始数据类型内存
- byte:1字节
- boolean:1字节
- char:2字节
- short:2字节
- int:4字节
- float:4字节
- long:8字节
- double:8字节
内存分配位置:
方法体内定义,在栈上分配
类的成员变量,在堆上分配
类的静态成员变量,在堆上分配(Java7之前在方法区中分配)
对象类型内存结构
对象由对象头(Mark Word与类型指针),实际数据,填充字节所组成:
- Mark Word:包含哈希码,锁状态标志,线程持有的锁,偏向线程id,gc分代年龄等
- 类型指针:虚拟机通过这个指针来确定这个对象是哪个类的实例
- 对象实际数据:对象所有成员变量
- 对齐填充:按照8的整数倍填充
Integer内存占用:
32位虚拟机:4+4+4+4=16字节。64位虚拟机不用压缩指针:8+8+4+4=24字节,使用压缩指针:8+4+4+0=16字节
Vector、ArrayList、LinkedList
三者都是有序集合,实现了List接口
区别:
- Vector:基于对象数组,随机访问速度较快,非尾部插入、删除速度较慢(移动后续所有元素),数组满时自动扩容1倍,使用synchronized实现线程安全
- ArrayList:基于对象数组,随机访问速度较快,非尾部插入、删除速度较慢(移动后续所有元素),数组满时自动扩容0.5倍,非线程安全
- LinkedList:基于双向链表,插入、删除比动态数组高效,随机访问速度比动态数组慢,不需要调整容量,非线程安全
其他Collection集合容器
- Set:不允许重复的集合
- TreeSet:支持自然顺序遍历,添加、删除、包含等操作时间复杂度O(log(n))
- HashSet:不保证有序,基于哈希算法,添加、删除、包含等操作在无哈希冲突的情况下时间复杂度O(1)
- LinkedHashSet:支持插入顺序遍历,基于双向链表,添加、删除、包含等操作性能也保证了O(1),由于需要维护链表的开销,所以性能略低于HashSet
- Queue/Deque:标准队列结构,支持FIFO/LIFO
- LinkedList:既是List也是Deque
- PriorityQueue:支持优先级顺序排列,基于二叉堆,时间复杂度O(log(n))
Collections工具类中提供了Collections.synchronizedList等一系列相关方法,可以用来实现基本的线程安全集合
Java默认排序算法
- 小数据集:直接使用二分插入排序
- 原始数据类型:目前使用双轴快速排序(Dual-Pivot QuickSort),是一种改进的快速排序算法,早期版本是相对传统的快速排序
- 对象数据类型:目前使用TimSort,是一种归并和二分插入排序(binarySort)结合的优化排序算法,思路是查找数据集中已经排好序的分区(这里叫 run),然后合并这些分区来达到排序的目的
- Java8并行排序:parallelSort方法,充分利用多核处理器的计算能力,底层实现基于fork-join框架,数据集在数万或百万以上时提升巨大
Map容器
Map容器以键值对的形式存储和操作数据的容器类型
- Hashtable:使用synchronized实现线程安全,key、value都不能为null
- HashMap:不保证有序,基于哈希算法,添加、删除、包含等操作在无哈希冲突的情况下时间复杂度O(1),非线程安全,key、value可以为null
- TreeMap:基于红黑树支持顺序访问,具体顺序由Comparator或Comparable(键的自然顺序)决定,操作时间复杂度O(log(n)),未实现Comparator接口时key不能为null
- LinkedHashMap:支持插入顺序或操作顺序(accessOrder)遍历,基于双向链表,操作性能也保证了O(1),根据操作顺序排列可以用于实现LRU缓存
Hash容器需要遵守的约定:
- equals相同,hashCode也一定要相同
- 重写equals后最好也要重写hashCode
Tree容器同样需要遵守的约定:compareTo的返回值需要和equals一致
IO、NIO、NIO2概念
- IO:同步阻塞IO类库,也叫BIO,同时处理多个任务需要多线程技术,提供File、InputStream/OutputStream、Socket、HttpURLConnection等IO操作
- InputStream/OutputStream:用于读取或写入字节,例如操作图片文件
- BufferedInputStream/BufferedOutputStream:利用缓冲区将批量数据进行一次操作,避免频繁读写磁盘提高IO效率
- Reader/Writer:用于操作字符,增加了字符编解码等功能,适用于从文件中读取或者写入文本信息
- close:相关IO工具类都需要利用try-with-resources、try-finally等机制保证资源被明确关闭
- NIO:同步非阻塞IO类库,基于单线程轮循事件,提供Channel、Selector、Buffer等新的抽象,同时提供了更接近操作系统底层的高性能数据操作方式
- Buffer:高效的数据容器,所有原始类型都有相应的Buffer实现(布尔除外)
- Channel:用于支持批量IO操作的一种抽象,类似Linux系统上的文件描述符
- Selector:用于实现单线程对多Channel高效管理,可以检测到注册在Selector上的多个Channel中,是否有Channel处于就绪状态
- NIO2:异步非阻塞型IO类库,也叫AIO(Asynchronous IO),异步IO操作基于事件和回调机制
Copy方式
- IO:利用InputStream读取,OutputStream写入来实现,需要进行用户态与内核态的切换,读取先在内核态将数据读到内核缓存,再切换到用户态将数据从内核缓存读到用户缓存,写入也类似
- NIO:利用FileChannel的transferTo或transferFrom来实现,在Linux和Unix中使用零拷贝技术(zero-copy),不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝。另:读取文件后进行Socket发送同样享受该机制带来的提升
- Files:利用内置的几种copy方法来实现,Stream相关的copy,内部实现是Stream在用户态的读写,Path的copy实际实现在不同平台的底层JNI中,内部实现也是用户态空间读写
NIO Buffer
Buffer基本属性:
- capacity:Buffer容量,也就是数组的长度
- position:要操作的数据起始位置
- limit:相当于操作的限额。在读取或者写入时,limit 的意义很明显是不一样的。比如,读取操作时,很可能将 limit 设置到所容纳数据的上限;而在写入时,则会设置容量或容量以下的可写限度
- mark:记录上一次postion的位置,默认是0,便利性设计,非必须
基本使用操作:
- 使用allocate方法创建ByteBuffer,capacity是缓冲区大小,position是0,limit默认是capacity大小
- 写入几个字节后,position就会相应增大,但不会超过limit
- 读取已写入的数据,需要调用flip,limit设置为当前的position,然后position设置为0
- 再次读取已写入数据,可以调用rewind,limit不变,position再次设置为0
Direct Buffer
- Direct Buffer:堆外Buffer,使用allocateDirect方法创建
- MappedByteBuffer:本质上也是种DirectBuffer,可以使用FileChannel.map创建。直接将文件按照指定大小映射为内存区域,程序将直接操作该区域数据,省去了内核空间到用户空间传输的损耗
Direct Buffer特点:
- 生命周期内内存地址都不会再发生更改,进而内核可以安全地对其进行访问,很多IO操作会很高效
- 减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高
- 创建和销毁过程比一般堆内Buffer开销大,所以通常用于长期使用、数据较大的场景
Direct Buffer垃圾收集:
Direct Buffer垃圾收集机制基于Cleaner和幻想引用(PhantomReference),本身不是public类型,内部实现了一个Deallocator负责销毁的逻辑
它的垃圾回收往往会在full GC时,其他大多数垃圾收集过程都不会被回收。因此建议显式地调用System.gc()来强制触发回收,或是重复使用Direct Buffer,或者自己调用释放方法(类似Netty)
接口和抽象类
- 接口:对行为的抽象,抽象方法的集合,主要用于API定义和实现分离
- 实现interface使用implements关键词
- 任何field都隐含着public static final意义
- 只能定义抽象方法或静态方法,没有非静态方法
- 一个类可以实现多个接口
- 抽象类:用abstract关键字修饰的类,不能实例化,主要用于代码重用
- 继承abstract class使用extends关键词
- 可以没有也可以有一个或多个抽象方法
- 抽取共用方法或共同成员变量后通过继承方式达到代码复用
- 一个类只能继承一个抽象类
- java8接口改进:
- 增加了functional interface用于支持Lambda,比如Runnable、Callable
- 增加了default method用于支持接口的默认方法实现,比如Collections的stream
面向对象
基本要素:
- 封装:隐藏事务内部实现细节,提高安全性和简化编程
- 继承:代码复用的基础
- 多态:重写(override)、重载(overload)、向上转型
设计原则(S.O.L.I.D):
- 单一职责(Single Responsibility):类或对象职责单一,某个类承担多重义务可考虑拆分
- 开关原则(Open-Close):对扩展开放,对修改关闭,避免新增同类功能而修改已有实现
- 里氏替换(Liskov Substitution):子类可以扩展父类功能,但不能改变父类原有的功能,凡是可以用父类或者基类的地方,都可以用子类替换
- 接口分离(Interface Segregation):类和接口设计时,定义为功能单一的多个接口,而不是一个接口定义多种功能
- 依赖反转(Dependency Inversion):实体应该依赖于抽象而不是实现