动态获取程序信息以及动态调用对象的功能称为 Java 语言的反射机制。
在运行时判断任意一个对象所属的类; 在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和 方法;在运行时调用任意一个对象的方法;生成动态代理。
好处:反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
不过,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
同步和异步关注的是*消息通信机制* (synchronous communication/ asynchronous communication)。同步,就是调用某个东西是,调用方得等待这个调用返回结果才能继续往后执行。异步,和同步相反 当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态来通知调用者,或通过回调函数处理这个调用。
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态. 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到 结果之后才会返回。 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
Java BIO: 同步并阻塞 (传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不作任何事情会造成不必要的线程开销。
BIO问题分析
每个请求都需要创建独立的线程,与对应的客户端进行数据处理。
当并发数大时,需要 创建大量线程来处理连接 ,系统资源占用较大。
连接建立后,如果当前线程暂时没有数据可读,则当前线程会一直阻塞在 Read 操作上,造成线程资源浪费。
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量
Java NIO: 同步非阻塞 ,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求会被注册到多路复用器上,多路复用器轮询到有 I/O 请求就会进行处理。具体做法是多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。 用户进程也需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问。
NIO 和 BIO 对比
BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。
BIO 是阻塞的,而 NIO 是非阻塞的。
BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中
AIO (异步非阻塞 I/O) 用户进程只需要发起一个 IO 操作然后立即返回,等 IO 操作真正的完成以后,应用程序会得到 IO 操作完成的通知。
BIO、NIO、AIO适用场景分析
BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求 比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务 器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。
长连接指建立Socket连接后不管是否使用都保持连接,但安全性较差。
设计模式是用来解决某一特定问题的通用解决方案
过度的设计可能会导致程序变得复杂
Integer x = new Integer(100);
Integer y = new Integer(100);
System.out.println(x == y); // false
Integer z = Integer.valueOf(100);
Integer k = Integer.valueOf(100);
System.out.println(z == k); // true
String这个类是被final修饰的,String类的值是保存在value数组中的,并且value数组是被private final修饰的,并且String 类没有提供修改这个字符串的方法。
1、private修饰,表明外部的类是访问不到value的,同时子类也访问不到,当然String类不可能有子类,因为类被final修饰了
2、final修饰,表明value的引用是不会被改变的,而value只会在String的构造函数中被初始化,而且并没有其他方法可以修改value数组中的值,保证了value的引用和值都不会发生变化
所以我们说String类是不可变的。
为什么要有常量池?
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
新版的变化
Java 9 将 String 的底层实现由 char[] 改成了 byte[]
StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
StringBuilder、StringBuffer 在缓冲区上的区别:
StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。而
StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。
StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
String
中的 equals
方法是被重写过的,比较的是 String 字符串的值是否相等。 Object
的 equals
方法是比较的对象的内存地址。
方便了实现字符串常量池
String 经常作为参数,String 不可变性可以保证参数不可变
譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
保证了线程的安全
加快字符串处理速度,因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
参考答案
List>
可以表示各种泛型List的父类,意思是元素类型未知的List;List super T>
用于设定类型通配符的下限,此处 ? 代表一个未知的类型,但它必须是T的父类型;List extends T>
用于设定类型通配符的上限,此处 ? 代表一个未知的类型,但它必须是T的子类型。扩展阅读
在Java的早期设计中,允许把Integer[]数组赋值给Number[]变量,此时如果试图把一个Double对象保存到该Number[]数组中,编译可以通过,但在运行时抛出ArrayStoreException异常。这显然是一种不安全的设计,因此Java在泛型设计时进行了改进,它不再允许把 List
对象赋值给 List
变量。
数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型,但G
不是 G
的子类型。Foo[]自动向上转型为Bar[]的方式被称为型变,也就是说,Java的数组支持型变,但Java集合并不支持型变。Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。
wait notifyAll notify getClass hashcode equals clone toString
==
对于基本类型和引用类型的作用效果是不同的:
==
比较的是值。==
比较的是对象的内存地址。equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()
方法存在于Object
类中,而Object
类是所有类的直接或间接父类,因此所有的类都有equals()
方法。
equals()
方法存在两种使用情况:
equals()
方法 :通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object
类equals()
方法。equals()
方法 :一般我们都重写 equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。equals如果不重写的话,默认的是“==”,对于引用对象来说比较的是内存地址,在业务中通过比较的是内容是否相等,所以需要重写equals
如果equals相等的话,那么根据java中的规则,对象的hashcode也是相等的
Hashcode方法是返回对象的内存地址映射为hash值,所以如果没有重写hashCode()方法,任何对象的hashCode()方法都是不相等的。
这是因为在一些容器(比如 HashMap
、HashSet
)中,有了 hashCode()
之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HastSet
的过程)!
我们在前面也提到了添加元素进HastSet
的过程,如果 HashSet
在对比的时候,同样的 hashCode
有多个对象,它会继续使用 equals()
来判断是否真的相同。也就是说 hashCode
帮助我们大大缩小了查找成本。
Exception 和Error都是继承于Throwable 类,在Java中只有Throwable类型的实例才能被程序抛出(throw)或者捕获(catch),它是异常处理机制的基本类型
Error,它表示程序无法处理的错误。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。会终止JVM的运行
Exception,它表示程序可能捕捉或者程序可以处理的异常。其中异常类 Exception 又分为运行时异常(RuntimeException)和编译时异常。编译时异常就是 ,Java 代码在编译过程中,如果受检查异常没有被 catch
或者throws
关键字处理的话,就没办法通过编译。运行时异常就是Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。比如:NullPointerException
(空指针错误)、ArrayIndexOutOfBoundsException
(数组越界错误)等
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
JDK是java开发工具包,[Java Development Kit],包括了Java运行环境(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。
bin:最主要的是编译器(javac.exe)
include:java和JVM交互用的头文件
lib:类库
jre:java运行环境
JRE(Java Runtime Environment,Java运行环境),包含JVM标准实现及Java核心类库。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)
双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。
(1) 如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给
父类加载器去完成;
(2) 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终到达顶层的启动类加
载器;
(3) 如果父类加载器可以完成类加载任务,就成功返回;如果父加载器无法完成这个加载任务时,子加载
器才会尝试自己去加载。
总结来说:
(1)查看已加载的类,如果没有,就委派上级,在上级查找。
(2)如果在上级中都没有找到,就在自己类加载器中找
(3)如果在上级中找到,就在上级中进行加载
优点:(1)避免类重复加载;(2)保护程序的安全,防止核心 API 被修改(沙箱安全机制);使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
Ps: Java 虚拟机是如何判定两个 Java 类是相同的:
(1) 全名要相同
(2) 加载此类的类加载器要一样
只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码, 被不同的类加载器加载之后所得到的类,也是不同的。
双亲委派模型是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object 这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object 类,而且这些类之间是不兼容的。通过双亲委派模型,对于 Java 核心库的类的加载工作由启动类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
需要继承ClassLoader类或URLClassLoader,并至少重写其中的findClass(String name)方法,若想打破双亲委托机制,需要重写loadClass方法
主要加载:自己指定路径的class文件
在java中并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。
父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。
从设计目的上来说,二者有如下的区别:
**接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务;**对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
抽象类体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。
从使用方式上来说,二者有如下的区别:
总结:接口体现的是一种规范,而抽象类体现的是一种模板设计,从使用方法来看,接口中不能包含普通方法、普通成员变量,构造方法和初始化块,但是在抽象类中都可以包含,其中抽象类的构造方法并不是用于创建对象,而是让其子类调用这些构造器来完成抽象类的初始化操作。一个java类只能继承一个父类,但是可以实现多个接口,这样可以弥补Java单继承的不足
扩展阅读
接口和抽象类很像,它们都具有如下共同的特征:
抽象类的应用场景一般用于抽取不同事物的共有特性,然后可以用接口实现不同事物的不同行为。遇到需求,先分析不同的对象是否有共同点,比如门,都可以开和关,这个就可以抽取出来定义一个抽象类,而有的门有门铃可以摁,有的门没有,那就将有门铃的对象实现摁门铃这个动作的接口。
IO(Input Output)用于实现对数据的输入与输出操作,Java把不同的输入/输出源(键盘、文件、网络等)抽象表述为流(Stream)。流是从起源到接收的有序数据,有了它程序就可以采用同一方式访问不同的输入/输出源。
final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。 finally 是 Java 保证重点代码一定要被执行的一种机制。
Java 泛型(generics) 是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。
泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
泛型接口:
public interface Generator<T> {
public T method();
}
泛型方法
public static void printArray(E[] inputArray) {
for (E element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
使用:
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray(intArray);
printArray(stringArray);