目录
类
接口
接口声明了一组能力。一个接口可能对应多个实现类。接口方法不需要加修饰符,加与不加相当于都是public abstract。
接口降低了耦合,提高了灵活性。使用接口的代码依赖的是接口本身,而非实现接口的具体类型,程序可以根据情况替换接口的实现,而不影响接口使用者
在Java 8之前,接口中的方法都是抽象方法,都没有实现体,Java 8允许在接口中定义两类新方法:静态方法和默认方法,它们有实现体
抽象类
具体类有直接对应的对象,而抽象类没有,它表达的是抽象概念。樱桃是具体对象,而水果则是抽象概念。
抽象方法只有声明,没有实现。接口中不能定义实例变量,而抽象类可以,一个类可以实现多个接口,但只能继承一个类。
应用
接口声明能力,抽象类提供默认实现,实现全部或部分方法,一个接口经常有一个对应的抽象类。如JDK
中List
接口和对应的AbstractList
抽象类。对于需要实现接口的具体类而言,有两个选择:一个是实现接口,自己实现全部方法;另一个则是继承抽象类,然后根据需要重写方法。继承的好处是复用代码,只重写需要的部分即可,需要编写的代码比较少,容易实现。不过,如果这个具体类已经有父类了,那就只能选择实现接口了。
内部类
内部类与包含它的外部类有比较密切的关系,定义在类内部,可以实现对外部完全隐藏,有更好的封装性。
每个内部类最后都会被编译为一个独立的类,生成一个独立的字节码文件
静态内部类
如果与外部类关系密切,且不依赖于外部类实例,主要是类内部使用的,就可以定义为一个静态内部类。则可以考虑定义为静态内部类。它可以访问外部类的静态变量和方法,但不可以访问实例变量和方法。
在JDK
中使用案例:Integer:IntegerCache,LinkedList:Node
成员内部类
与静态内部类不同,除了静态变量和方法,成员内部类还可以直接访问外部类的实例变量和方法
集合
Java 集合框架主要包括两种类型的容器,一种是Collection,存储元素集合,另一种是Map,存储键/值对映射。
Collection 接口又有根据容器的不同特点划分出 3 种子类型:List(按照顺序保存元素)、Set(集合:不保存重复的元素) 和 Queue(队列:先进先出),接下来用抽象类(实现了一些公共的逻辑(设计模式当中的模板模式)),最后是具体实现类。
Map 接口有四个主要的实现类:HashMap
、HashTable
(线程安全)、LinkedHashMap
(保存了记录的插入顺序)、TreeMap
(把它保存的记录根据键排序)
HashMap
只需要
O(1)
的时间就可以把一个元素存入或读出,根据键快速查找到对应的值。
底层原理
结构:数组+链表+红黑树(
JDK1.8
)设计实现:
1、待存入的元素需要一个能计算自己哈希值的函数(根类:
Object
中的hashCode
函数)2、哈希表根据每个元素的哈希值把它存储到合适的位置,把哈希值转换成数组下标可以采用的方法是对数组的长度求余数。
哈希冲突
如果将两个哈希值不同的元素存入数组中的同一位置,就会引起冲突。为了解决这种冲突,可以把存入数组中同一位置的多个元素用链表存储(链地址法)。如果在哈希表中添加更多的元素,那么就会有更多的冲突,有更多的元素被存入同一个链表中,链表越长查找需要的时间就越长,这就违背了设计哈希表的一个初衷:存入和读取一个元素的时间复杂度为O(1)。所以,当哈希表中元素的数目与数组长度的比值超过某一阈值时,就对数组进行扩容,把哈希表中的所有的元素重新分配位置。
HashMap
线程安全问题
HashMap
不是并发安全的,在并发更新的情况下,HashMap
可能出现死循环,占满CPU。
死循环出现在多个线程同时扩容哈希表的时候
I/O
I/O本质是将什么样的数据写到什么地方。所以传输数据的格式和传输数据的方式会影响的I/O的效率。Java的I/O操作类在包java.io
下
传输数据的数据格式
- 基于字节:
InputStream
和OutputStream
- 基于字符:Writer和Reader
传输数据的方式
- 基于磁盘:File
- 基于网络:Socket
Q&A
为什么有操作字符的I/O接口?
以InputStream
/OutputStream
为基类的流基本都是以二进制形式处理数据的,不能够方便地处理文本文件,没有编码的概念,能够方便地按字符处理文本数据的基类是Reader和Writer
序列化和反序列化
序列化就是将内存中的Java对象持久保存到一个流中,反序列化就是从流中恢复Java对象到内存。序列化和反序列化主要有两个用处:一是对象状态持久化,二是网络远程调用,用于传递和返回对象。
- Java主要通过接口
Serializable
和类ObjectInputStream
/ObjectOutputStream
提供对序列化的支持 - 缺点:列化后的形式比较大、浪费空间,序列化/反序列化的性能也比较低,是Java特有的技术,不能与其他语言交
1、将字段声明为transient,默认序列化机制将忽略该字段,不会进行保存和恢复
2、定义版本号:在序列化时,会将该版本号写入流,在反序列化时,会将流中的值与类定义中的版本号进行比较,如果不匹配,会抛出InvalidClassException
- 其他方式:文本格式:
XML
和JSON
、二进制:ProtoBuf
、Thrift
、MessagePack
静态域与静态方法
- 静态域:static修饰,每个类中只有一个这样的域。属于类(所有的对象共享),而不属于任何独立的对象。
- 实例域:每一个对象对于所有的实例域都有自己的一份拷贝
- 静态方法:静态方法是一种不能向对象实施操作的方法,可以认为静态方法是没有this参数的方法。使用:(1)一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)
(2)一个方法只需要访问类的静态域
泛型
类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。代码与它们能够操作的数据类型不再绑定在一起,同一套代码可以用于多种数据类型
- 作用:复用代码,降低耦合,保证类型安全,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
- 原理:类型擦除:Java 在编译期间,所有的泛型信息都会被擦掉,替换成Object类型
- 使用:泛型类、泛型接口、泛型方法。
并发
线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈(虚拟机栈、本地方法栈)。
Q&A
为什么调用的是start,执行的却是run方法
start表示启动该线程,使其成为一条单独的执行流,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行,执行的起点就是run方法
线程间的通信
线程间的通信是指线程之间以何种机制来交换信息,Java采用共享内存模型进行通信。
- Java内存模型
在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。
局部变量,方法定义参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见,
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。一般情况下,线程会先读取/修改本地内存中的变量副本,之后在将值刷到共享内存当中,因此在多线程中可能引起内存可见性问题。
- volatile
保证了共享变量的"可见性"。当一个线程修改一个共享变量时,另外一个线程能够立即读到这个修改的值。
- synchronized
Java中的每一个对象都可以作为锁。当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
synchronized
修饰实例方法,锁是当前的实例对象。
修饰静态方法,锁是当前类的Class
对象。对于同步方法块,锁是synchronized
括号当中配置的对象。
- 实现原理:在JVM中实现,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,
monitorenter
指令是在编译后插入到同步代码块的开始位置,
而monitorexit
是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter
指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。- Java对象头:synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果对象是非数组类型,则用2字宽存储对象头
线程池
由于创建/销毁线程需要消耗系统资源,使用线程池可以复用已创建的线程,控制并发的数量。
组成:任务队列、工作者线程
- 构造函数参数
- corePoolSize:核心线程数最大值(核心线程默认情况下会一直存在于线程池中,而非核心线程如果长时间的闲置,就会被销毁)
- maximumPoolSize:线程总数最大值
- keepAliveTime:非核心线程闲置超时时长
- keepAliveTime的单位
- BlockingQueue:阻塞队列,维护着等待执行的Runnable任务对象。
- ThreadFactory: 创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数
- RejectedExecutionHandler:拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略
AQS(AbstractQueuedSynchronizer
):抽象队列同步器
抽象类,只实现一些主要逻辑,有些方法由子类实现;实现了同步的功能;使用先进先出(FIFO)队列存储数据。
反射
在运行时,而非编译时,动态获取类型的信息,比如接口信息、成员信息、方法信息、构造方法信息等,根据这些动态获取到的信息创建对象、访问/修改成员、调用方法
Class类
每个已加载的类在内存都有一份类信息,每个对象都有指向它所属类信息的引用。类信息对应的类:java.lang.Class
,所有类的根父类Object有一个方法,可以获取对象的Class对象
- 根据类名直接加载Class,获取Class对象
Class<?> cls = Class.forName("java.util.HashMap")
- 通过Class对象可以获取名称信息、字段信息(Field)、方法信息(Method)、创建对象和构造方法(Constructor)、类型信息、声明信息
缺点
- 反射更容易出现运行时错误,使用显式的类和接口,编译器能帮我们做类型检查,但使用反射,类型是运行时才知道的
- 反射的性能要低一些,在访问字段、调用方法前,反射先要查找对应的Field/Method,要慢一些。
注解
注解是给程序添加一些信息,这些信息用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等。注解可以被编译器、程序运行时和其他工具使用,用于增强或修改程序行为
内置注解
@Override、@Deprecated、@SuppressWarnings(都是给编译器用的,所以@Retention都是Retention-Policy.SOURCE)
声明式风格
使用声明式风格使得程序员可以在更高的抽象层次上思考和解决问题,而不是陷于底层的细节实现
声明关键字和语法本身。系统/框架/库,它们负责解释、执行声明式的语句。应用程序,使用声明式风格写程序。
注解的创建
- @Target表示注解的目标,如果没有声明@Target,默认为适用于所有类型
- @Retention表示注解信息保留到什么时候
- 注解内参数类型:合法的类型有基本类型、String、Class、枚举、注解,以及这些类型的数组。参数定义时可以使用default指定一个默认值
查看注解的信息
@Retention为RetentionPolicy.RUNTIME的注解,可以利用反射机制在运行时进行查看和利用这些信息。
- Class、Field、Method、Constructor类中都有获取注解(getAnnotations、getParameterAnnotations)、判断是否具有指定注解(isAnnotationPresent)的方法
类加载
类加载器ClassLoader就是加载其他类的类,它负责将字节码文件加载到内存,创建Class对象
应用场景
- 热部署。在不重启Java程序的情况下,动态替换类的实现。
- 应用的模块化和相互隔离。不同的ClassLoader可以加载相同的类但互相隔离、互不影响。
- 从不同地方灵活加载。系统默认的ClassLoader一般从本地的.class文件或jar文件中加载字节码文件,通过自定义的ClassLoader,我们可以从共享的Web服务器、数据库、缓存服务器等其他地方加载字节码文件
类加载器种类
- 引导类加载器(Bootstrap ClassLoader):这个加载器是Java虚拟机实现的一部分,一般是C语言实现的,它负责加载Java的基础类,主要是<JAVA_HOME>/lib/rt.jar,日常用的Java类库比如String、ArrayList等都位于该包内
- 扩展类加载器(Extension ClassLoader):这个加载器的实现类是sun.misc.Laun-cher$ExtClassLoader,它负责加载Java的一些扩展类,一般是<JAVA_HOME>/lib/ext目录中的jar包
- 系统类加载器(Application ClassLoader):这个加载器的实现类是sun.misc. Launcher$AppClassLoader,它负责加载应用程序的类,包括自己写的和引入的第三方法类库,即所有在类路径中指定的类。
类加载的流程
负责加载类的类就是类加载器,它的输入是完全限定的类名,输出是Class对象
- 1、判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个Class-Loader加载一次。
- 2、如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象。
- 3、在父ClassLoader没有加载成功的前提下,自己尝试加载类。
Q&A
“双亲委派”模型
- 优先让父ClassLoader去加载,可以避免Java类库被覆盖的问题。例如,当要求系统类加载器(Application ClassLoader)加载一个系统类(比如,java.util.ArrayList)时,它首先要求扩展类加载器(Extension ClassLoader)进行加载,该扩展类加载器则首先要求引导类加载器(Bootstrap ClassLoader)进行加载。
代理
调用者不直接调用实际对象,而是调用代理对象,由代理对象来访问实际对象,可以在代理类中动态的添加逻辑,如权限控制等。
静态代理
代理类手动创建,一般要实现同一个接口。
动态代理
代理类动态生成
JDK
代理
代理一组接口,实现公共逻辑。
CGLIB
代理
正则表达式
正则表达式是一串字符,它描述了一个文本模式,利用它可以方便地处理文本,包括文本的查找、替换、验证、切分等。
函数式编程
Lambda
表达式:一种紧凑的传递代码的方式。针对常见的集合数据处理,Java 8
引入了一套新的类库,位于包java.util.stream
下,称为Stream API
。
个人博客:https://www.kangpeiqin.cn/#/index