# java面试问题
# java基本类型与包装类
基本数据类型 | byte | boolean | char | short | int | float | long | double |
---|---|---|---|---|---|---|---|---|
包装类 | Byte | Boolean | Character | Short | Integer | Float | Long | Double |
# 包装类和基本类型的区别
- 包装类和基本类型:包装类是对象,拥有方法和字段, 对象的调用都是通过引用对象的地址,基本类型不是
- 参数传递:包装类型是引用的传递,基本类型是值的传递
- 声明不同:包装类型需要 new 在堆内存进行 new 来分配内存空间,基本数据类型不需要 new 关键字
- 存储位置不同:包装类型是把对象放在堆中,然后通过对象的引用来调用,基本数据类型直接将值保存在值栈中
- 初始值不同: 包装类型的初始值为 null,int 的初始值为 0、boolean 的初始值为false
- 使用方式不同:包装类型是在集合如 coolection,Map时会使用,基本数据类型直接赋值使用就好.
# ==和equals的区别
基本数据类型 | 基本数据类型 | 包装类 |
---|---|---|
== | 比较的值是否相同 | 比较地址值否相同 |
equal | 如果没有对equals 方法进行重写,则比较的是引用类型的变量所指向的对象的地址;但是String、Date 等类对 equals 方法进行了重写的话,比较的是所指向的对象的内容 |
# String、StringBuffer、StringBuilder
- 都是继承与 AbstractStringBuilder
- string 定长度的不变效率最小
- StringBuffer会自动进行扩容工作,扩展为原数组长度的 2 倍加 2。线程安全/效率其次
- StringBuilder会自动进行扩容工作,扩展为原数组长度的 2 倍加 2。线程不安全/ 效率最高
StringBuffer()的初始容量可以容纳 16 个字符,当该对象的实体存放的字符的长度大于16 时, 实体容量就自动增加。StringBuffer 对象可以通过 length()方法获取实体中存放的字符序列长度, 通过 capacity()方法来获取当前实体的实际容量。底层都是一个字符数组的来实现的。 Stringbuffer 中就是比 Stringbuder 多了 synchronize 关键字。
# final与static的区别
final | static |
---|---|
都可以修饰类、方法、成员变量 | 都可以修饰类、方法、成员变量。 |
都不能用于修饰构造方法。 | 都不能用于修饰构造方法 |
final 不可以修饰代码块 | static可以修饰类的代码块 |
final可以修饰方法内的局部变量 | static不可以修饰方法内的局部变量 |
final 修饰表示常量、一旦创建不可改变 | static修饰表示静态或全局,被修饰的属性和方法属于类,可以用类名.静态属性/方法名访问 |
final 标记的成员变量必须在声明的同时赋值,或在该类的构造方法中赋值,不可以重新赋值 | static 修饰的代码块表示静态代码块,当 Java 虚拟机(JVM)加载类时,就会执行该代码块,只会被执行一次 |
final 方法不能被子类重写 | static 修饰的属性,也就是类变量,是在类加载时被创建并进行初始化,只会被创建一次 |
final 类不能被继承,没有子类,final 类中的方法默认是 final的 | static 修饰的变量可以重新赋值。 static 方法不能被重写,static 方法中不能用 this 和 super 关键字 |
# final finally finalize区别
- final 可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值.
- finally 一般作用在 try-catch 代码块中,在处理异常的时候,通常我们将一定要执行的代码方法,finally 代码块中,表示不管是否出现异常,该代码块都会执行,一般用存放一些关闭资源的代码
- finalize 是一个方法,属于 Object 类的一个方法,而 Object 类是所有类的父类,该方法一般由垃圾回收器来调, 当我们调用 System.gc() 方法的时候,由垃圾回收器调用 finalize(),回收垃圾,一个对象是否可回收的最后判断.
# 父子类的加载机制
静态变量先于静态代码块执行,整个执行顺序是:
- 父类静态变量初始化。
- 父类静态代码块。
- 子类静态变量初始化。
- 子类静态语句块。
- 父类变量初始化。
- 父类代码块。
- 父类构造函数。
- 子类变量初始化。
- 子类语句块。
- 子类构造函数。
# 普通类和抽象类区别?
抽象类 :不能被实例化,有抽象方法,抽象方法只需申明,无需实现,含有抽象方法的类必须申明为抽象类, 抽象类的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类,抽象方法不能被声明为静态, 抽象方法不能用 private 修饰,抽象方法不能用 final 修饰
普通类 可以实例化
# 抽象类和接口的区别
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。(单继承多实现方式)
- 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。实现接口或继承抽象类的普通子类都必须实现这些抽象方法 接口的特点:
- 只能包含抽象方法,静态方法和默认方法,不能为普通方法提供方法实现(在 JDK1.8 可以使用default 和 static 关键字来修饰接口中定义的普通方法)
- 接口中的成员变量只能是 public static final 类型
- 接口不能包含构造器
- 接口里不能包含初始化块
抽象类的特点:
- 完全可以包含普通方法,接口中的普通方法默认为抽象方法
- 抽象类中的成员变量可以是各种类型的
- 抽象类可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
- 抽象类里完全可以包含初始化块
# JAVA内部类的对象
Java 类中不仅可以定义变量和方法,还可以定义类,这样定义在类内部的类就被称为内部类。 根据定义的方式不同,内部类分为静态内部类,成员内部类,局部内部类,匿名内部类四种
静态内部类
可以访问外部类所有的静态变量和方法,即使是 private 的也一样。静态内部类和一般类一致,
可以定义静态变量、方法,构造方法等。其它类使用静态内部类需要使用“外部类.静态内部类”方式,
如下所示:Out.Inner inner =new Out.Inner();inner.print();
成员内部类
定义在类内部的非静态类,就是成员内部类。成员内部类不能定义静态方法和变 量(final 修饰的除外)。这是因为成员内部类是非静态的, 类初始化的时候先初始化静态成员, 如果允许成员内部类定义静态变量,那么成员内部类的静态变量初始化顺序是有歧义的。 局部内部类:定义在方法中的类,就是局部类。如果一个类只在某个方法中使用,则可以考虑使用局部类
匿名内部类
要继承一个父类或者实现一个接口、直接使用 new来生成一个对象的引用匿名内部类 我们必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一个接口。 同时它也是没有 class 关键字,这是因为匿名内部类是直接使用 new 来生成一个对象的引用
# 类的实例化创建
- 运用反射,调用 java.lang.Class 或 java.lang.reflect.Constructor 类的newInstance() 方法
- 调用对象的 clone() 方法
- 运用反序列化手段,调用 java.io.ObjectInputStream 对象的 readObject() 方法、不会调用构造函数
// 使用 new 关键字//创建对象方式 1:使用 new 关键字
User u1 = new User("1",2,"3");
System.err.println(u1.toString());
//创建对象方式 2:使用反射//发射方式创建对象要求被创建的对象编写空构造
try {
User u2 = User.class.newInstance();
System.err.println(u2.toString());
} catch (InstantiationException | IllegalAccessException e) {
System.out.println("反射创建失败"+e.getMessage());
}
// 使用clone方法创建对象:要求被创建或者被克隆的对象实现 Cloneable 接口,是在内存上对已有对象的影印,所以不会调用构造函数
try {
User u3 = (User) u1.clone();
System.err.println("u3:"+u3.toString());
System.out.println(u1==u3);//false
} catch (CloneNotSupportedException e) {
System.out.println("克隆创建失败"+e.getMessage());
}
# 深拷贝和浅拷贝的原理
- 浅拷贝:当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
- 深拷贝:除了对象本身被复制外,对象所包含的所有成员变量也将复制.
# 线程的状态
Java 中的线程五有种状态分别是:创建、就绪,运行、挂起、结束。
- 运行态:进程实际占用 cpu 的时间的运行时
- 就绪态:可以运行的 但是其他线程在运行而处于就绪态
- 阻塞态:除非某种外部事件发生,否则进程不能运行
进程通信 | 线程通信 |
---|---|
socket通信 | wait/notify 等待 |
消息队列 | Volatile 内存共享 |
信号量 | CountDownLatch 并发工具 |
共享内存 | CyclicBarrier 并发工具 |
管道通信 |
# 线程的调度策略
线程调度器选择优先级最高的线程运行,如果发生以下情况,就会终止线程的运行:
- 线程体中调用了yield方法让出了对cpu的占用权利
- 线程体中调用了sleep方法使线程进入睡眠状态
- 线程由于 IO 操作受到阻塞
- 另外一个更高优先级线程出现
- 在支持时间片的系统中,该线程的时间片用完
# 线程的创建方式
- 继承 Thread 类创建线程
- 实现 Runnable 接口创建线程
- 使用 Callable 和 Future 创建线程
- 使用线程池例如用 Executor 框架
# 线程同步的方法
- 同步方法,有 synchronized 关键字修饰的方法。 由于 java 的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。 在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized 关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
- 同步代码块:即有 synchronized 关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。 注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用 synchronized 代码块同步关键代码即可。
- 使用局部变量实现线程同步。如果使用 ThreadLocal 管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立, 这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
- 使用特殊域变量(volatile)实现线程同步
- volatile 关键字为域变量的访问提供了一种免锁机制
- 使用 volatile 修饰域相当于告诉虚拟机该域可能会被其他线程更新
- 因此每次使用该域就要重新计算,而不是使用寄存器中的值
- volatile 不会提供任何原子操作,它也不能用来修饰 final 类型的变量
- 在 java 中新增了一个 java.util.concurrent 包来支持同步。ReentrantLock 类是可重入、互斥、实现了 Lock 接口的锁, 它与使用 synchronized 方法和快具有相同的基本行为和语义,并且扩展了其能力。
- wait 与 notify 关键字的来控制线程的同步。
- 使用阻塞队列实现线程同步,前面 5 种同步方式都是在底层实现的线程同步,
- 使用原子变量实现线程同步。在 java 的 util.concurrent.atomic 包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。
# 线程池的种类
newCachedThreadPool缓存线程池底层:
返回ThreadPoolExecutor实例,corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE, keepAliveTime为60L,unit为TimeUnit.SECONDS,workQueue为SynchronousQueue(同步队列) 通俗:当有新任务到来,则插入到 SynchronousQueue 中,由于 SynchronousQueue 是同步队列, 因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务; 若池中线程空闲时间超过指定大小,则该线程会被销毁。 适用:执行很多短期异步的小程序或者负载较轻的服务器
newFixedThreadPool固定数量的线程池
底层:返回ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;
keepAliveTime为0L(不限时),unit为:TimeUnit.MILLISECONDS,WorkQueue为:new LinkedBlockingQueue<Runnable>()
无解阻塞队列
通俗:创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了,如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
适用:执行长期的任务,性能好很多
newSingleThreadExecutor单个线程的线程池
底层:FinalizableDelegatedExecutorService 包 装 的 ThreadPoolExecutor 实例 ,
corePoolSize为1,maximumPoolSize为1,keepAliveTime 为 0L,unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue<Runnable>()
无解阻塞队列
通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
适用:一个任务一个任务执行的场景
NewScheduledThreadPool延时缓存线程池
底层:创建ScheduledThreadPoolExecutor实例 , corePoolSize为传递来的参数,maximumPoolSize为 Integer.MAX_VALUE; keepAliveTime 为 0;unit 为:TimeUnit.NANOSECONDS;workQueue 为:new DelayedWorkQueue()一个按超时时间升序排序的队列 通俗:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周 期性任务执行,如果所有线程均处于繁忙状态, 对于新任务会进入 DelayedWorkQueue 队列中,这是一种按照超时时间排序的队列结构 适用:周期性执行任务的场景
# 五种线程池的使用场景
- newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。
- newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。
- newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。
- newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
- newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用 cpu数量的线程来并行执行。
# 程序错误分为三种:
- 编译错误: 是因为程序没有遵循语法规则。throws用在函数上,后面跟的是异常类,可以跟多个;而throw用在函数内,后面跟的是异常对象。
- 运行时错误: 是因为程序在执行时,运行环境发现了不能执行的操作。(空指针异常、数组越界异常、SQL 异常、非法参数异常、找不到类文件异常等)
- 逻辑错误: 是因为程序没有按照预期的逻辑顺序执行。异常也就是指程序运行时发生错误,而异常处理就是对这些错误进行处理和控制。
# 内存溢出和内存泄露
- 内存溢出是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。
- 内存泄露是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费。 Java 中一般不会产生内存泄露,因为有垃圾回收器自动回收垃圾,但这也不绝对,当我们new了对象,并保存了其引用, 但是后面一直没用它,而垃圾回收器又不会去回收它,这边会造成内存泄露。
# Java内存溢出的情况
- Java 堆溢出(对象数量到达最大堆的容量限制后就会产生内存溢出异常)
- 虚拟机栈和本地方法栈溢出。
- 方法区和运行时常量池溢出
- 本机直接内存溢出,DirectMemory 容量可通过-XX: MaxDirectMemorySize 指定,如果不指定,则默认与 Java 堆最大值 (-Xmx 指定)一样。
# 代码导致OOM的原因:
- 检查代码中是否有死循环或递归调用。
- 检查是否有大循环重复产生新对象实体。
- 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。 这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
- 检查 List、MAP 等集合对象是否有使用完后,未清除的问题。List、MAP 等集合对象会始终存有对对象的引用,使得这些对象不能被 GC 回收。
# 解决OOM方法:
增加jvm的内存大小。方法有:
- 在执行某个class文件时候,可以使用java -Xmx256Maa.class 来设置运行aa.class时jvm 所允许占用的最大内存为256M。
- 对tomcat容器,可以在启动时对jvm设置内存限度。对 tomcat,可以在catalina.bat 中添加
- 对resin容器,同样可以在启动时对 JVM 设置内存限度。在 bin 文件夹下创建一个startup.bat 文件,
- 优化程序,释放垃圾。主要包括避免死循环,应该及时释放种资源。
# 反射机制原理
JAVA 反射机制是在运行状态中,对于任意一个类,获取任意类的名称、package 信息、所有属性、方法、注解、类型、类加载器、 modifiers(public、static)、父类、现实接口等:对于任意一个对象,都能够调用它的任意一个方法和属性; 这种动态获取类信息以及动态调用对象内容就称为java语言的反射机制。
我们知道,要使用一个类,就要先把它加载到虚拟机中,生成一个Class 对象。这个class对象就保存了这个类的一切信息。
反射机制的实现,就是获取这个Class对象,通过class 对象去访问类、对象的元数据以及运行时的数据。
有三种方法获得类的Class对象:Class.forName(String className)、className.class、实例对象.getClass();
Java的反射机制:操作的就是这个对象的.class 文件,首先加载相应类的字节码(运行 eclipse的时候,.class 文件的字节码会加载到内存中), 随后解剖(反射 reflect)出字节码中的构造函数、方法以及变量(字段),或者说是取出。
# 获取反射的三种方法
通过 new 对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制在JDK中,主要由以下类来实现Java反射机制,除了Class类,一般位于java.lang.reflect 包中 java.lang.reflect 包中 java.lang.Class :一个类 java.lang.reflect.Field :类的成员变量(属性) java.lang.reflect.Method :类的成员方法 java.lang.reflect.Constructor :类的构造方法 java.lang.reflect.Array :提供了静态方法动态创建数组,访问数组的元素
# 反射机制的应用场景
例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的Spring/Hibernate等框架也大量使用到了反射机制。
- 我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动
- Spring框架也用到很多反射机制,最经典的就是xml的配置模式。
- Spring通过XML配置模式装载Bean的过程:
- 将程序内所有XML或Properties 配置文件加载入内存中.
- Java 类里面解析xml或properties 里面的内容,得到对应实体类的字节码字符串以及相关的属性信息.
- 使用反射机制,根据这个字符串获得某个类的 Class 实例.
- 动态配置实例的属性.
# Java的序列化和反序列
它是处理对象流的一种机制,即可以很方便的保存内存中 java 对象的状态,同时也为了方便传输。
JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。
比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff
,这些都是基于二进制的序列化协议。
像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
序列化和反序列的作用
- 常用于不同进程之间的对象传输,方便传输,速度快,还很安全,被调用方序列化,调用方反序列化即可拿到传输前最原始的java 对象
- 方便存储,不管是存储成文件还是数据库都行,存储为文件,下回要用可以直接反序列拿到对象。
为什么要序列化
有序在不同的虚拟机中的,我们常常设计到 A 中的程序调用 B 中的程序,这个时候会出现问题的是怎么样A中没有 B 的对象。 需要通过网络传输才能得到 B 的对象,或者内存数据。
怎么来解决问题
将B中的序列化的数据经过序列化处理,传输到 A 中的,再利用但序列化的操作,生成一个 B 中的对象。并可以实现在A中的程序中的调用B中的方法。
实现的方法
有java中自带的serializable
的接口。利用的protocol buffer
的序列化分方式。第三是采用thirft
的序列化的方式。
序列化的步骤
- 创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流: Object OutputStreamoos=new Object OutputStream(newFileOutputStream("D:\object.out"));
- 通过对象输出流的 writeObject()方法写对象: oos.writeObject(new User("xuliugen", "123456", "male"));
反序列化的步骤
- 创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流: Object InputStream ois=new Object InputStream(new FileInputStream("object.out"));
- 通过对象输出流的 readObject()方法读取对象: User user = (User) ois.readObject();
为什么不推荐使用 JDK 自带的序列化
- 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
- 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
- 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
# 什么是字节码
在Java中,JVM可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。 Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。 所以,Java 程序运行时相对来说还是高效的(不过,和C++,Rust,Go 等语言还是有一定差距的), 由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
我们需要格外注意的是.class->机器码这一步。在这一步JVM类加载器首先加载字节码文件, 然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码), 所以后面引进了JIT(just-in-time compilation)编译器,而 JIT 属于运行时编译。 当JIT编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。 而我们知道,机器码的运行效率肯定是高于Java解释器的。这也解释了我们为什么经常会说Java是编译与解释共存的语言。
:::Tips HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码), 而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多, 它的速度就越快。JDK9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码, 这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。 :::
AOT 可以提前编译节省启动时间,那为什么不全部使用这种编译方式呢?
长话短说,这和 Java 语言的动态特性有千丝万缕的联系了。举个例子,CGLIB 动态代理使用的是 ASM 技术, 而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 .class 文件,如果全部使用 AOT 提前编译, 也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用JIT即时编译器。
编译型 :编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快, 开发效率比较低。常见的编译性语言有 C、C++、Go、Rust等等。
解释型 :解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快, 执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP等等。
# 重载和重写有什么区别?
重载
- 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理。发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
- 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
- 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
- 构造方法无法被重写
# 包装类型的缓存机制了解么?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。 如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据, Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较
# 自动装箱与拆箱吗?
很多人会有疑问,既然 Java 中为了提高效率,提供了八种基本数据类型,为什么还要提供包装类呢? 这个问题,其实前面已经有了答案,因为 Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将 int 、double 等类型放进去的。因为集合的容器要求元素是 Object 类型。 为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
拆箱与装箱
有了基本数据类型和包装类,肯定有些时候要在他们之间进行转换。比如把一个基本数据类型的 int 转换成一个包装类型的 Integer 对象。
我们认为包装类是对基本类型的包装,所以,把基本数据类型转换成包装类的过程就是打包装,英文对应于 boxing,中文翻译为装箱。
反之,把包装类转换成基本数据类型的过程就是拆包装,英文对应于 unboxing,中文翻译为拆箱。
在 Java SE5 之前,要进行装箱,可以通过以下代码: Integer i = new Integer(10);
在 Java SE5 中,为了减少开发人员的工作,Java 提供了自动拆箱与自动装箱功能。
- 自动装箱: 就是将基本数据类型自动转换成对应的包装类。
- 自动拆箱:就是将包装类自动转换成对应的基本数据类型。
Integer i = 10; //自动装箱
int b = i; //自动拆箱
Integer i=10 可以替代 Integer i = new Integer(10);,这就是因为 Java 帮我们提供了自动装箱的功能,不需要开发者手动去 new 一个 Integer 对象。
自动装箱与自动拆箱的实现原理
既然 Java 提供了自动拆装箱的能力,那么,我们就来看一下,到底是什么原理,Java 是如何实现的自动拆装箱功能。 我们有以下自动拆装箱的代码:
public static void main(String[]args){
Integer integer=1; //装箱
int i=integer; //拆箱
}
对以上代码进行反编译后可以得到以下代码:
public static void main(String[]args){
Integer integer=Integer.valueOf(1);
int i=integer.intValue();
}
从上面反编译后的代码可以看出,int 的自动装箱都是通过 Integer.valueOf() 方法来实现的,Integer 的自动拆箱都是通过 integer.intValue 来实现的。
如果读者感兴趣,可以试着将八种类型都反编译一遍 ,你会发现以下规律:自动装箱都是通过包装类的 valueOf() 方法来实现的.自动拆箱都是通过包装类对象的 xxxValue() 来实现的
哪些地方会自动拆装箱
- 场景一、将基本数据类型放入集合类
我们知道,Java 中的集合类只能接收对象类型,那么以下代码为什么会不报错呢?
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i ++){
li.add(i);
}
将上面代码进行反编译,可以得到以下代码:
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2){
li.add(Integer.valueOf(i));
}
- 场景二、包装类型和基本类型的大小比较
有没有人想过,当我们对 Integer 对象与基本类型进行大小比较的时候,实际上比较的是什么内容呢?看以下代码:
Integer a = 1;
System.out.println(a == 1 ? "等于" : "不等于");
Boolean bool = false;
System.out.println(bool ? "真" : "假");
对以上代码进行反编译,得到以下代码:
Integer a = 1;
System.out.println(a.intValue() == 1 ? "等于" : "不等于");
Boolean bool = false;
System.out.println(bool.booleanValue ? "真" : "假");
可以看到,包装类与基本数据类型进行比较运算,是先将包装类进行拆箱成基本数据类型,然后进行比较的。
- 场景三、包装类型的运算
- 场景四、三目运算符的使用
- 场景五、函数参数与返回值
自动拆装箱与缓存
Java SE 的自动拆装箱还提供了一个和缓存有关的功能,我们先来看以下代码,猜测一下输出结果:
public static void main(String... strings) {
Integer integer1 = 3;
Integer integer2 = 3;
if (integer1 == integer2)
System.out.println("integer1 == integer2");
else
System.out.println("integer1 != integer2");
Integer integer3 = 300;
Integer integer4 = 300;
if (integer3 == integer4)
System.out.println("integer3 == integer4");
else
System.out.println("integer3 != integer4");
}
--------输出结果--------
integer1 == integer2
integer3 != integer4
我们普遍认为上面的两个判断的结果都是 false。虽然比较的值是相等的,但是由于比较的是对象, 而对象的引用不一样,所以会认为两个 if 判断都是 false 的。在 Java 中,== 比较的是对象引用, 而 equals 比较的是值。所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回 false。 奇怪的是,这里两个类似的 if 条件判断返回不同的布尔值。
原因就和 Integer 中的缓存机制有关。在 Java 5 中,在 Integer 的操作上引入了一个新功能来节省内存和提高性能。 整型对象通过使用相同的对象引用实现了缓存和重用。适用于整数值区间 -128 至 +127。只适用于自动装箱。使用构造函数创建对象不适用。
我们只需要知道,当需要进行自动装箱时,如果数字在 -128 至 127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象。 其中的 Javadoc 详细的说明了缓存支持 -128 到 127 之间的自动装箱过程。最大值 127 可以通过 -XX:AutoBoxCacheMax=size 修改。 实际上这个功能在 Java 5 中引入的时候,范围是固定的 -128 至 +127。后来在 Java 6 中,可以通过 java.lang.Integer.IntegerCache.high 设置最大值。
这使我们可以根据应用程序的实际情况灵活地调整来提高性能。到底是什么原因选择这个-128 到 127 范围呢?因为这个范围的数字是最被广泛使用的。 在程序中,第一次使用 Integer 的时候也需要一定的额外时间来初始化这个缓存。
在 Boxing Conversion 部分的 Java 语言规范(JLS)规定如下:如果一个变量 p 的值是:
- -128 至 127 之间的整数 (§3.10.1)
- true 和 false 的布尔值 (§3.10.3)
- \u0000 至 \u007f 之间的字符 (§3.10.4)
范围内的时,将 p 包装成 a 和 b 两个对象时,可以直接使用 a == b 判断 a 和 b 的值是否相等。
自动拆装箱带来的问题
当然,自动拆装箱是一个很好的功能,大大节省了开发人员的精力,不再需要关心到底什么时候需要拆装箱。但是,他也会引入一些问题。
包装对象的数值比较,不能简单的使用 ==,虽然 -128 到 127 之间的数字可以,但是这个范围之外还是需要使用 equals 比较。 前面提到,有些场景会进行自动拆装箱,同时也说过,由于自动拆箱,如果包装类对象为 null ,那么自动拆箱时就有可能抛出 NPE。 如果一个 for 循环中有大量拆装箱操作,会浪费很多资源。
# 什么是可变长参数?
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。 就比如下面的这个 printVariable 方法就可以接受 0 个或者多个参数。
public static void method1(String... args) {
//......
}
另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。
public static void method2(String arg1, String... args) {
//......
}
遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
public class VariableLengthArgument {
public static void printVariable(String... args) {
for (String s : args) {
System.out.println(s);
}
}
public static void printVariable(String arg1, String arg2) {
System.out.println(arg1 + arg2);
}
public static void main(String[] args) {
printVariable("a", "b");
printVariable("a", "b", "c", "d");
}
}
--------------------------------------------
输出
ab
a
b
c
d
# Java 泛型(Generics)
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该 ArrayList 对象只能传入Person对象,如果传入其他类型的对象就会报错。
ArrayList<E> extends AbstractList<E>
并且,原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
Generic<Integer> genericInteger = new Generic<Integer>(123456);
泛型接口
public interface Generator<T> {
public T method();
}
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
泛型方法
public static <E> 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 );
项目中哪里用到了泛型?
- 自定义接口通用返回结果
CommonResult<T>
通过参数 T 可根据具体的返回类型动态指定结果的数据类型 - 定义 Excel 处理类
ExcelUtil<T>
用于动态指定 Excel 导出的数据类型 - 构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。
# SPI机制
SPI即ServiceProviderInterface
,字面意思就是:“服务提供者的接口”,专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。
SPI 和 API 有什么区别
说到SPI就不得不说一下API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:
一般模块之间都是通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是API ,这种接口和实现都是放在实现方的。
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
SPI 的优缺点?
通过 SPI 机制能够大大地提高接口设计的灵活性,
但是 SPI 机制也存在一些缺点,
- 比如:需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个 ServiceLoader 同时 load 时,会有并发问题。# 序列化和反序列化
# 什么是SPI?
SPI全称是Service Provider Interface ,它是JDK内置的一种动态扩展点的实现。简单来说,就是我们可以定义一个标准的接口,然后第三方的库里面可以实现这个接口。 那么,程序在运行的时候,会根据配置信息动态加载第三方实现的类,从而完成功能的动态扩展机制。
在Java里面,SPI机制有一个非常典型的实现案例,就是数据库驱动java.jdbc.Driver,JDK里面定义了数据库驱动类Driver,它是一个接口,JDK并没有提供实现。 具体的实现是由第三方数据库厂商来完成的。在程序运行的时候,会根据我们声明的驱动类型,来动态加载对应的扩展实现,从而完成数据库的连接。
除此之外,在很多开源框架里面都借鉴了Java SPI的思想,提供了自己的SPI框架,比如Dubbo定义了ExtensionLoader,实现功能的扩展。Spring提供了SpringFactoriesLoader,实现外部功能的集成。
# 有哪些常见的IO模型
UNIX 系统下, IO 模型一共有 5 种:同步阻塞 I/O
、同步非阻塞 I/O
、I/O 多路复用
、信号驱动 I/O
和异步 I/O
。这也是我们经常提到的 5 种 IO 模型。
Java 中 3 种常见IO模型
BIO 属于同步阻塞IO模型
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。在客户端连接数量不高的情况下,是没问题的。 但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO(Non-blocking/NewI/O)
Java中的NIO于Java 1.4中引入,对应java.nio包,提供了Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking, 不单纯是New。它是支持面向缓冲的,基于通道的I/O操作方法。对于高负载、高并发的(网络)应用,应使用NIO。
Java 中的NIO可以看作是I/O多路复用模型。也有很多人认为,Java中的NIO属于同步非阻塞IO模型。
同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。 相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。但是, 这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。这个时候,I/O 多路复用模型 就上场了。
IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。 目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,目前几乎在所有的操作系统上都有支持。
- select 调用:内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
- epoll 调用:linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。
IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
AIO(Asynchronous I/O)
AIO 也就是NIO 2。Java7中引入了NIO的改进版 NIO2,它是异步IO模型。异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。 目前来说 AIO的应用还不是很广泛。Netty之前也尝试使用过 AIO,不过又放弃了。这是因为,Nett使用了AIO 之后,在Linux系统上的性能并没有多少提升。
# 过滤器与拦截器原理
Filter基于函数回调
- 它可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次.
- 目的:是用来做一些过滤操作,获取我们想要的数据,比如:JavaWeb中对传入的request、response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者Controller进行业务逻辑操作。
- 场景:修改字符编码(CharacterEncodingFilter)、过滤HttpServletRequest中敏感字符(XSSFilter自定义过滤器)、
- 配置方式:web.xml
拦截器
- 基于Java的反射机制,属于面向切面编程(AOP)的一种运用,就是在Service或者一个方法前调用一个方法,或者在方法后调用一个方法,甚至在抛出异常的时候做业务逻辑的操作。
- 由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个Controller生命周期之内可以多次调用。
- 但缺点是只能对Controller请求进行拦截,也可以拦截静态资源,必须要添加上配置才可以避免静态资源被拦截,拦截器不能拦截的只有jsp。执行顺序:过滤前-----拦截前-----Action 处理-----拦截后-----过滤后
- Spring mvc的文件中配置
监听器
- 监听器主要用来监听只用。通过listener可以监听web服务器中某一个执行动作,并根据其要求作出相应的响应。
- Servlet 的监听器 Listener,它是实现了javax.servlet.ServletContextListener接口的服务器端程序,它也是随web应用的启动而启动,只初始化一次,随web应用的停止而销毁。
- 在web.xml中配置
# Javaweb9大对象
- request对象:客户端的请求信息被封装在 request 对象中,通过它才能了解到客户的需求,然后做出响应。它是HttpServletRequest类的实例。
- response对象: response 对象包含了响应客户请求的有关信息,但在 JSP 中很少直接用到它。它是HttpServletResponse类的实例。
- session对象: session 对象指的是客户端与服务器的一次会话,从客户连到服务器的一个WebApplication开始,直到客户端与服务器断开连接为止。 是HttpSession 类的实例.
- out对象: out对象是JspWriter类的实例,是向客户端输出内容常用的对象.
- page对象就是指向当前JSP页面本身,有点象类中的this指针,它是java.lang.Object类的实例.
- application 对象实现了用户间数据的共享,可存放全局变量。它开始于服务器的启动,直到服务器的关闭,在此期间,此对象将一直存在; 这样在用户的前后连接或不同用户之间的连接中,可以对此对象的同一属性进行操作;在任何地方对此对象属性的操作,都将影响到其他用户对此的访问。 服务器的启动和关闭决定了application对象的生命。它是ServletContext类的实例。
- exception对象是一个例外对象,当一个页面在运行过程中发生了例外,就产生这个对象。如果一个 JSP 页面要应用此对象,就必须把isErrorPage设为 true,否则无法编译。他实际上是 java.lang.Throwable 的对象
- pageContext对象,pageContext 对象提供了对 JSP 页面内所有的对象及名字空间的访问,也就是说他可以访问到本页所在的SESSION,也可以取本页面所在的application 的某一属性值,他相当于页面中所有功能的集大成者,它的本类名也叫pageContext。
- config对象: config对象是在一个Servlet初始化时,JSP引擎向它传递信息用的,此信息包括Servlet初始化时所要用到的参数(通过属性名和属性值构成)以及服务器的有关信息(通过传递一个ServletContext对象)
# JavaIO类
InputStream/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
# 如何避免sql注入
- 严格限制 Web 应用的数据库的操作权限,给连接数据库的用户提供满足需要的最低权限,最大限度的减少注入攻击对数据库的危害
- 校验参数的数据格式是否合法(可以使用正则或特殊字符的判断)
- 对进入数据库的特殊字符进行转义处理,或编码转换
- 预编译 SQL(Java 中使用 PreparedStatement),参数化查询方式,避免 SQL 拼接
- 发布前,利用工具进行 SQL 注入检测
- 报错信息不要包含 SQL 信息输出到 Web 页面 Spring
# JDK1.8的新特性
- Lambda 表达式
- 函数式接口
- 函数式接口
- Stream API
- 新时间日期 API:LocalDate 、 LocalTime 、 LocalDateTime
# finally块一定会执行吗?
finally语句块在两种情况下不会执行:
- 程序没有进入到try语句块因为异常导致程序终止,这个问题主要是开发人员在编写代码的时候,异常捕获的范围不够。
- 在try或者cache语句块中,执行了System.exit(0)语句,导致JVM直接退出
# HashMap中的hash方法为什么要右移16位异或?
之所以要对hashCode无符号右移16位并且异或,核心目的是为了让hash值的散列度更高,尽可能减少hash表的hash冲突,从而提升数据查找的性能。 在HashMap的put方法里面,是通过Key的hash值与数组的长度取模计算得到数组的位置。而在绝大部分的情况下,n的值一般都会小于2^16次方,也就是65536。
所以也就意味着i的值,始终是使用hash值的低16位与(n-1)进行取模运算,这个是由与运算符&的特性决定的。 这样就会造成key的散列度不高,导致大量的key集中存储在固定的几个数组位置,很显然会影响到数据查找性能。
因此,为了提升key的hash值的散列度,在hash方法里面,做了位移运算。首先使用key的hashCode无符号右移16位,意味着把hashCode的高位移动到了低位。 然后再用hashCode与右移之后的值进行异或运算,就相当于把高位和低位的特征进行和组合。从而降低了hash冲突的概率。
# Thread和Runnable的区别
Thread是一个类,Runnable是接口,因为在Java语言里面的继承特性,接口可以支持多继承,而类只能单一继承。 所以如果在已经存在继承关系的类里面要实现线程的话,只能实现Runnable接口。
Runnable表示一个线程的顶级接口,Thread类其实是实现了Runnable这个接口,我们在使用的时候都需要实现run方法。
站在面向对象的思想来说,Runnable相当于一个任务,而Thread才是真正处理的线程, 所以我们只需要用Runnable去定义一个具体的任务,然后交给Thread去处理就可以了,这样达到了松耦合的设计目的。
接口表示一种规范或者标准,而实现类表示对这个规范或者标准的实现,所以站在线程的角度来说,Thread才是真正意义上的线程实现。 Runnable表示线程要执行的任务,因此在线程池里面,提交一个任务传递的类型是Runnable。
# 什么是守护线程
守护线程,它是一种专门为用户线程提供服务的线程,它的生命周期依赖于用户线程。只有JVM中仍然还存在用户线程正在运行的情况下,守护线程才会有存在的意义。 否则,一旦JVM进程结束,那守护线程也会随之结束。也就是说,守护线程不会阻止JVM的退出。但是用户线程会!
守护线程和用户线程的创建方式是完全相同的,我们只需要调用用户线程里面的setDaemon方法并且设置成true,就表示这个线程是守护线程。
因为守护线程拥有自己结束自己生命的特性,所以它适合用在一些后台的通用服务场景里面。比如JVM里面的垃圾回收线程,就是典型的使用场景。 这个场景的特殊之处在于,当JVM进程技术的时候,内存回收线程存在的意义也就不存在了。所以不能因为正在进行垃圾回收导致JVM进程无法技术的问题。 但是守护线程不能用在线程池或者一些IO任务的场景里面,因为一旦JVM退出之后,守护线程也会直接退出。就会可能导致任务没有执行完或者资源没有正确释放的问题。
# 对序列化和反序列化的理解
我认为,之所以需要序列化,核心目的是为了解决网络通信之间的对象传输问题。也就是说,如何把当前JVM进程里面的一个对象,跨网络传输到另外一个JVM进程里面。 而序列化,就是把内存里面的对象转化为字节流,以便用来实现存储或者传输。反序列化,就是根据从文件或者网络上获取到的对象的字节流,根据字节流里面保存的对象描述信息和状态。重新构建一个新的对象。
其次呢,序列化的前提是保证通信双方对于对象的可识别性,所以很多时候,我们会把对象先转化为通用的解析格式,比如json、xml等。然后再把他们转化为数据流进行网络传输,从而实现跨平台和跨语言的可识别性。
我再补充一下序列化选择。市面上开源的序列化技术非常多,比如Json、Xml、Protobuf、Kyro、hessian等等。那在实际应用里面,哪种序列化最合适,我认为有几个关键因素。
- 序列化之后的数据大小,因为数据大小会影响传输性能
- 序列化的性能,序列化耗时较长会影响业务的性能
- 是否支持跨平台和跨语言
- 技术的成熟度,越成熟的方案使用的公司越多,也就越稳定。
# new String("abc")到底创建了几个对象?
- 如果 abc 这个字符串常量不存在,则创建两个对象,分别是 abc 这个字符串常量,以及 new String 这个实例对象。
- 如果 abc 这字符串常量存在,则只会创建一个对象
# Java SPI是什么?
Java SPI,全称是Service Provider Interface。它是一种基于接口的动态扩展机制,相当于Java里面提供了一套接口。然后第三方可以实现这个接口来完成功能的扩展和实现。
在Java的SDK里面,提供了一个数据库驱动的接口java.sql.Driver
。它的作用是提供数据库的访问能力。不过,在Java里面并没有提供实现,因为不同的数据库厂商,
会有不同的语法和实现。所以只能由第三方数据库厂商来实现,比如Oracle是oracle.jdbc.OracleDriver
,mysql是com.mysql.jdbc.Driver
.
然后在应用开发的时候,根据集成的驱动实现连接到对应数据库。
Java中SPI机制主要思想是将装配的控制权移到程序之外实现标准和实现的解耦,以及提供动态可插拔的能力,在模块化的设立中,这种思想非常重要。实现Java SPI,需要满足几个基本的格式
- 需要先定义一个接口,作为扩展的标准.
- 在classpath目录下创建
META-INF/service
文件目录. - 在这个目录下,以接口的全限定名命名的配置文件, 文件内容是这个接口的实现类.
- 在应用程序里面,使用ServiceLoad,就可以根据接口名称找到classpath所有的扩展时间
- 然后根据上下文场景选择实现类完成功能的调用。
Java SPI有一定的不足之处,比如,不能根据需求去加载扩展实现,每次都会加载扩展接口的所有实现类并进行实例化,实例化会造成性能开销,并且加载一些不需要用到的实现类,会导致内存资源的浪费。
# new Integer(112)和Integer.valueOf(112)的区别
- new Integer,是创建一个Integer对象实例。
- Integer.valueOf(112),Integer默认提供了Cache机制,在-128到127区间范围内的数据,通过valueOf方法不需要创建新的对象实例,只需要从缓存中获取即可。
# 转发与重定向的区别
地址栏
- 转发:不变,不会显示出转向的地址
- 重定向:会显示转向之后的地址
请求
- 转发:一次请求
- 重定向:至少提交了两次请求
数据:
- 转发:对request对象的信息不会丢失,因此可以在多个页面交互过程中实现请求数据的共享
- 重定向:request信息将丢失
原理:
- 转发:是在服务器内部控制权的转移,是由服务器区请求,客户端并不知道是怎样转移的,因此客户端浏览器的地址不会显示出转向的地址。
- 重定向:是服务器告诉了客户端要转向哪个地址,客户端再自己去请求转向的地址,因此会显示转向后的地址,也可以理解浏览器至少进行了两次的访问请求。
转发和重定向的流程
- 重定向的流程: 浏览器发送请求->服务器运行->相应请求------->,返回给浏览器一个新的地址与响应码,浏览器进行判断为重定向,自动发送一个新的请求给服务器,请求地址为刚刚服务器发送给浏览器的地址。->服务器运行->相应请求
- 转发的流程:发送请求 -->服务器运行–>进行请求的重新设置,例如通过request.setAttribute(name,value)–>根据转发的地址,获取该地址的网页–>响应请求给浏览器
← 分布式限流原理与设计 Mysql面试问题 →