共计 31710 个字符,预计需要花费 80 分钟才能阅读完成。
本文曾经收录到 Github 仓库,该仓库蕴含 计算机根底、Java 根底、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享 等外围知识点,欢送 star~
Github 地址:https://github.com/Tyson0314/Java-learning
Java 的特点
Java 是一门面向对象的编程语言。面向对象和面向过程的区别参考下一个问题。
Java 具备平台独立性和移植性。
- Java 有一句口号:
Write once, run anywhere
,一次编写、到处运行。这也是 Java 的魅力所在。而实现这种个性的正是 Java 虚拟机 JVM。已编译的 Java 程序能够在任何带有 JVM 的平台上运行。你能够在 windows 平台编写代码,而后拿到 linux 上运行。只有你在编写完代码后,将代码编译成.class 文件,再把 class 文件打成 Java 包,这个 jar 包就能够在不同的平台上运行了。
Java 具备稳健性。
- Java 是一个强类型语言,它容许扩大编译时查看潜在类型不匹配问题的性能。Java 要求显式的办法申明,它不反对 C 格调的隐式申明。这些严格的要求保障编译程序能捕获调用谬误,这就导致更牢靠的程序。
- 异样解决是 Java 中使得程序更持重的另一个特色。异样是某种相似于谬误的异样条件呈现的信号。应用
try/catch/finally
语句,程序员能够找到出错的解决代码,这就简化了出错解决和复原的工作。
Java 是如何实现跨平台的?
Java 是通过 JVM(Java 虚拟机)实现跨平台的。
JVM 能够了解成一个软件,不同的平台有不同的版本。咱们编写的 Java 代码,编译后会生成.class 文件(字节码文件)。Java 虚拟机就是负责将字节码文件翻译成特定平台下的机器码,通过 JVM 翻译成机器码之后能力运行。不同平台下编译生成的字节码是一样的,然而由 JVM 翻译成的机器码却不一样。
只有在不同平台上装置对应的 JVM,就能够运行字节码文件,运行咱们编写的 Java 程序。
因而,运行 Java 程序必须有 JVM 的反对,因为编译的后果不是机器码,必须要通过 JVM 的翻译能力执行。
Java 与 C++ 的区别
- Java 是纯正的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 兼容 C,岂但反对面向对象也反对面向过程。
- Java 通过虚拟机从而实现跨平台个性,C++ 依赖于特定的平台。
- Java 没有指针,它的援用能够了解为平安指针,而 C++ 具备和 C 一样的指针。
- Java 反对主动垃圾回收,而 C++ 须要手动回收。
- Java 不反对多重继承,只能通过实现多个接口来达到雷同目标,而 C++ 反对多重继承。
JDK/JRE/JVM 三者的关系
JVM
英文名称(Java Virtual Machine),就是咱们耳熟能详的 Java 虚拟机。Java 可能跨平台运行的外围在于 JVM。
所有的 java 程序会首先被编译为.class 的类文件,这品种文件能够在虚拟机上执行。也就是说 class 文件并不间接与机器的操作系统交互,而是通过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地零碎执行。
针对不同的零碎有不同的 jvm 实现,有 Linux 版本的 jvm 实现,也有 Windows 版本的 jvm 实现,然而同一段代码在编译后的字节码是一样的。这就是 Java 可能跨平台,实现一次编写,多处运行的起因所在。
JRE
英文名称(Java Runtime Environment),就是 Java 运行时环境。咱们编写的 Java 程序必须要在 JRE 能力运行。它次要蕴含两个局部,JVM 和 Java 外围类库。
JRE 是 Java 的运行环境,并不是一个开发环境,所以没有蕴含任何开发工具,如编译器和调试器等。
如果你只是想运行 Java 程序,而不是开发 Java 程序的话,那么你只须要装置 JRE 即可。
JDK
英文名称(Java Development Kit),就是 Java 开发工具包
学过 Java 的同学,都应该装置过 JDK。当咱们装置完 JDK 之后,目录构造是这样的
能够看到,JDK 目录下有个 JRE,也就是 JDK 中曾经集成了 JRE,不必独自装置 JRE。
另外,JDK 中还有一些好用的工具,如 jinfo,jps,jstack 等。
最初,总结一下 JDK/JRE/JVM,他们三者的关系
JRE = JVM + Java 外围类库
JDK = JRE + Java 工具 + 编译器 + 调试器
Java 程序是编译执行还是解释执行?
先看看什么是编译型语言和解释型语言。
编译型语言
在程序运行之前,通过编译器将源程序编译成机器码可运行的二进制,当前执行这个程序时,就不必再进行编译了。
长处:编译器个别会有预编译的过程对代码进行优化。因为编译只做一次,运行时不须要编译,所以编译型语言的程序执行效率高,能够脱离语言环境独立运行。
毛病:编译之后如果须要批改就须要整个模块从新编译。编译的时候依据对应的运行环境生成机器码,不同的操作系统之间移植就会有问题,须要依据运行的操作系统环境编译不同的可执行文件。
总结:执行速度快、效率高;依附编译器、跨平台性差些。
代表语言:C、C++、Pascal、Object- C 以及 Swift。
解释型语言
定义:解释型语言的源代码不是间接翻译成机器码,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。在运行的时候才将源程序翻译成机器码,翻译一句,而后执行一句,直至完结。
长处:
- 有良好的平台兼容性,在任何环境中都能够运行,前提是装置了解释器(如虚拟机)。
- 灵便,批改代码的时候间接批改就能够,能够疾速部署,不必停机保护。
毛病:每次运行的时候都要解释一遍,性能上不如编译型语言。
总结:解释型语言执行速度慢、效率低;依附解释器、跨平台性好。
代表语言:JavaScript、Python、Erlang、PHP、Perl、Ruby。
对于 Java 这种语言,它的 源代码 会先通过 javac 编译成 字节码 ,再通过 jvm 将字节码转换成 机器码 执行,即解释运行 和编译运行配合应用,所以能够称为混合型或者半编译型。
面向对象和面向过程的区别?
面向对象和面向过程是一种软件开发思维。
- 面向过程就是剖析出解决问题所须要的步骤,而后用函数按这些步骤实现,应用的时候顺次调用就能够了。
- 面向对象是把形成问题事务分解成各个对象,别离设计这些对象,而后将他们组装成有残缺性能的零碎。面向过程只用函数实现,面向对象是用类实现各个功能模块。
以五子棋为例,面向过程的设计思路就是首先剖析问题的步骤:
1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤 2,9、输入最初后果。
把下面每个步骤用别离的函数来实现,问题就解决了。
而面向对象的设计则是从另外的思路来解决问题。整个五子棋能够分为:
- 黑白单方
- 棋盘零碎,负责绘制画面
- 规定零碎,负责断定诸如犯规、输赢等。
黑白单方负责承受用户的输出,并告知棋盘零碎棋子布局发生变化,棋盘零碎接管到了棋子的变动的信息就负责在屏幕下面显示出这种变动,同时利用规定零碎来对棋局进行断定。
面向对象有哪些个性?
面向对象四大个性:封装,继承,多态,形象
1、封装就是将类的信息暗藏在类外部,不容许内部程序间接拜访,而是通过该类的办法实现对暗藏信息的操作和拜访。良好的封装可能缩小耦合。
2、继承是从已有的类中派生出新的类,新的类继承父类的属性和行为,并能扩大新的能力,大大增加程序的重用性和易维护性。在 Java 中是单继承的,也就是说一个子类只有一个父类。
3、多态是同一个行为具备多个不同表现形式的能力。在不批改程序代码的状况下扭转程序运行时绑定的代码。实现多态的三要素:继承、重写、父类援用指向子类对象。
- 动态多态性:通过重载实现,雷同的办法有不同的參数列表,能够依据参数的不同,做出不同的解决。
- 动静多态性:在子类中重写父类的办法。运行期间判断所援用对象的理论类型,依据其理论类型调用相应的办法。
4、形象。把客观事物用代码形象进去。
面向对象编程的六大准则?
- 对象繁多职责:咱们设计创立的对象,必须职责明确,比方商品类,外面相干的属性和办法都必须跟商品相干,不能呈现订单等不相干的内容。这里的类能够是模块、类库、程序集,而不单单指类。
- 里式替换准则:子类可能齐全代替父类,反之则不行。通常用于实现接口时使用。因为子类可能齐全代替基(父)类,那么这样父类就领有很多子类,在后续的程序扩大中就很容易进行扩大,程序齐全不须要进行批改即可进行扩大。比方 IA 的实现为 A,因为我的项目需要变更,当初须要新的实现,间接在容器注入处更换接口即可.
- 迪米特法令,也叫最小准则,或者说最小耦合。通常在设计程序或开发程序的时候,尽量要高内聚,低耦合。当两个类进行交互的时候,会产生依赖。而迪米特法令就是倡议这种依赖越少越好。就像构造函数注入父类对象时一样,当须要依赖某个对象时,并不在意其外部是怎么实现的,而是在容器中注入相应的实现,既合乎里式替换准则,又起到理解耦的作用。
- 开闭准则:凋谢扩大,关闭批改。当我的项目需要产生变更时,要尽可能的不去对原有的代码进行批改,而在原有的根底上进行扩大。
- 依赖倒置准则:高层模块不应该间接依赖于底层模块的具体实现,而应该依赖于底层的形象。接口和抽象类不应该依赖于实现类,而实现类依赖接口或抽象类。
- 接口隔离准则:一个对象和另外一个对象交互的过程中,依赖的内容最小。也就是说在接口设计的时候,在遵循对象繁多职责的状况下,尽量减少接口的内容。
简洁版:
- 繁多职责:对象设计要求独立,不能设计万能对象。
- 开闭准则:对象批改最小化。
- 里式替换:程序扩大中形象被具体能够替换(接口、父类、能够被实现类对象、子类替换对象)
- 迪米特:高内聚,低耦合。尽量不要依赖细节。
- 依赖倒置:面向形象编程。也就是参数传递,或者返回值,能够应用父类类型或者接口类型。从狭义上讲:基于接口编程,提前设计好接口框架。
- 接口隔离:接口设计大小要适中。过大导致净化,过小,导致调用麻烦。
数组到底是不是对象?
先说说对象的概念。对象是依据某个类创立进去的一个实例,示意某类事物中一个具体的个体。
对象具备各种属性,并且具备一些特定的行为。站在计算机的角度,对象就是内存中的一个内存块,在这个内存块封装了一些数据,也就是类中定义的各个属性。
所以,对象是用来封装数据的。
java 中的数组具备 java 中其余对象的一些根本特点。比方封装了一些数据,能够拜访属性,也能够调用办法。
因而,能够说,数组是对象。
也能够通过代码验证数组是对象的事实。比方以下的代码,输入后果为 java.lang.Object。
Class clz = int[].class;
System.out.println(clz.getSuperclass().getName());
由此,能够看出,数组类的父类就是 Object 类,那么能够推断出数组就是对象。
Java 的根本数据类型有哪些?
- byte,8bit
- char,16bit
- short,16bit
- int,32bit
- float,32bit
- long,64bit
- double,64bit
- boolean,只有两个值:true、false,能够使⽤用 1 bit 来存储
简略类型 | boolean | byte | char | short | Int | long | float | double |
---|---|---|---|---|---|---|---|---|
二进制位数 | 1 | 8 | 16 | 16 | 32 | 64 | 32 | 64 |
包装类 | Boolean | Byte | Character | Short | Integer | Long | Float | Double |
在 Java 标准中,没有明确指出 boolean 的大小。在《Java 虚拟机标准》给出了单个 boolean 占 4 个字节,和 boolean 数组 1 个字节的定义,具体 还要看虚拟机实现是否依照标准来,因而 boolean 占用 1 个字节或者 4 个字节都是有可能的。
为什么不能用浮点型示意金额?
因为计算机中保留的小数其实是十进制的小数的近似值,并不是精确值,所以,千万不要在代码中应用浮点数来示意金额等重要的指标。
倡议应用 BigDecimal 或者 Long 来示意金额。
什么是值传递和援用传递?
- 值传递是对基本型变量而言的,传递的是该变量的一个正本,扭转正本不影响原变量。
- 援用传递个别是对于对象型变量而言的,传递的是该对象地址的一个正本,并不是原对象自身,两者指向同一片内存空间。所以对援用对象进行操作会同时扭转原对象。
java 中不存在援用传递,只有值传递。即不存在变量 a 指向变量 b,变量 b 指向对象的这种状况。
理解 Java 的包装类型吗?为什么须要包装类?
Java 是一种面向对象语言,很多中央都须要应用对象而不是根本数据类型。比方,在汇合类中,咱们是无奈将 int、double 等类型放进去的。因为汇合的容器要求元素是 Object 类型。
为了让根本类型也具备对象的特色,就呈现了包装类型。相当于将根本类型包装起来,使得它具备了对象的性质,并且为其增加了属性和办法,丰盛了根本类型的操作。
主动装箱和拆箱
Java 中根底数据类型与它们对应的包装类见下表:
原始类型 | 包装类型 |
---|---|
boolean | Boolean |
byte | Byte |
char | Character |
float | Float |
int | Integer |
long | Long |
short | Short |
double | Double |
装箱:将根底类型转化为包装类型。
拆箱:将包装类型转化为根底类型。
当根底类型与它们的包装类有如下几种状况时,编译器会 主动 帮咱们进行装箱或拆箱:
- 赋值操作(装箱或拆箱)
- 进行加减乘除混合运算(拆箱)
- 进行 >,<,== 比拟运算(拆箱)
- 调用 equals 进行比拟(装箱)
- ArrayList、HashMap 等汇合类增加根底类型数据时(装箱)
示例代码:
Integer x = 1; // 装箱 调⽤ Integer.valueOf(1)
int y = x; // 拆箱 调⽤了 X.intValue()
上面看一道常见的面试题:
Integer a = 100;
Integer b = 100;
System.out.println(a == b);
Integer c = 200;
Integer d = 200;
System.out.println(c == d);
输入:
true
false
为什么第三个输入是 false?看看 Integer 类的源码就晓得啦。
public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer c = 200;
会调用 调⽤ Integer.valueOf(200)
。而从 Integer 的 valueOf() 源码能够看到,这里的实现并不是简略的 new Integer,而是用 IntegerCache 做一个 cache。
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch(NumberFormatException nfe) {// If the property cannot be parsed into an int, ignore it.}
}
high = h;
}
...
}
这是 IntegerCache 动态代码块中的一段,默认 Integer cache 的上限是 -128,下限默认 127。当赋值 100 给 Integer 时,刚好在这个范畴内,所以从 cache 中取对应的 Integer 并返回,所以 a 和 b 返回的是同一个对象,所以 ==
比拟是相等的,当赋值 200 给 Integer 时,不在 cache 的范畴内,所以会 new Integer 并返回,当然 ==
比拟的后果是不相等的。
String 为什么不可变?
先看看什么是不可变的对象。
如果一个对象,在它创立实现之后,不能再扭转它的状态,那么这个对象就是不可变的。不能扭转状态的意思是,不能扭转对象内的成员变量,包含根本数据类型的值不能扭转,援用类型的变量不能指向其余的对象,援用类型指向的对象的状态也不能扭转。
接着来看 Java8 String 类的源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
}
从源码能够看出,String 对象其实在外部就是一个个字符,存储在这个 value 数组外面的。
value 数组用 final 润饰,final 润饰的变量,值不能被批改。因而 value 不能够指向其余对象。
String 类外部所有的字段都是公有的,也就是被 private 润饰。而且 String 没有对外提供批改外部状态的办法,因而 value 数组不能扭转。
所以,String 是不可变的。
那为什么 String 要设计成不可变的?
次要有以下几点起因:
- 线程平安。同一个字符串实例能够被多个线程共享,因为字符串不可变,自身就是线程平安的。
- 反对 hash 映射和缓存。因为 String 的 hash 值常常会应用到,比方作为 Map 的键,不可变的个性使得 hash 值也不会变,不须要从新计算。
- 出于平安思考。网络地址 URL、文件门路 path、明码通常状况下都是以 String 类型保留,假若 String 不是固定不变的,将会引起各种安全隐患。比方将明码用 String 的类型保留,那么它将始终留在内存中,直到垃圾收集器把它革除。如果 String 类不是固定不变的,那么这个明码可能会被扭转,导致呈现安全隐患。
- 字符串常量池优化。String 对象创立之后,会缓存到字符串常量池中,下次须要创立同样的对象时,能够间接返回缓存的援用。
既然咱们的 String 是不可变的,它外部还有很多 substring,replace,replaceAll 这些操作的办法。这些办法如同会扭转 String 对象?怎么解释呢?
其实不是的,咱们每次调用 replace 等办法,其实会在堆内存中创立了一个新的对象。而后其 value 数组援用指向不同的对象。
为何 JDK9 要将 String 的底层实现由 char[]改成 byte[]?
次要是为了 节约 String 占用的内存。
在大部分 Java 程序的堆内存中,String 占用的空间最大,并且绝大多数 String 只有 Latin- 1 字符,这些 Latin- 1 字符只须要 1 个字节就够了。
而在 JDK9 之前,JVM 因为 String 应用 char 数组存储,每个 char 占 2 个字节,所以即便字符串只须要 1 字节,它也要依照 2 字节进行调配,节约了一半的内存空间。
到了 JDK9 之后,对于每个字符串,会先判断它是不是只有 Latin- 1 字符,如果是,就依照 1 字节的规格进行分配内存,如果不是,就依照 2 字节的规格进行调配,这样便进步了内存使用率,同时 GC 次数也会缩小,晋升效率。
不过 Latin- 1 编码集反对的字符无限,比方不反对中文字符,因而对于中文字符串,用的是 UTF16 编码(两个字节),所以用 byte[]和 char[]实现没什么区别。
String, StringBuffer 和 StringBuilder 区别
1. 可变性
- String 不可变
- StringBuffer 和 StringBuilder 可变
2. 线程平安
- String 不可变,因而是线程平安的
- StringBuilder 不是线程平安的
- StringBuffer 是线程平安的,外部应用 synchronized 进行同步
什么是 StringJoiner?
StringJoiner 是 Java 8 新增的一个 API,它基于 StringBuilder 实现,用于实现对字符串之间通过分隔符拼接的场景。
StringJoiner 有两个构造方法,第一个结构要求顺次传入分隔符、前缀和后缀。第二个结构则只要求传入分隔符即可(前缀和后缀默认为空字符串)。
StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
StringJoiner(CharSequence delimiter)
有些字符串拼接场景,应用 StringBuffer 或 StringBuilder 则显得比拟繁琐。
比方上面的例子:
List<Integer> values = Arrays.asList(1, 3, 5);
StringBuilder sb = new StringBuilder("(");
for (int i = 0; i < values.size(); i++) {sb.append(values.get(i));
if (i != values.size() -1) {sb.append(",");
}
}
sb.append(")");
而通过 StringJoiner 来实现拼接 List 的各个元素,代码看起来更加简洁。
List<Integer> values = Arrays.asList(1, 3, 5);
StringJoiner sj = new StringJoiner(",", "(", ")");
for (Integer value : values) {sj.add(value.toString());
}
另外,像平时常常应用的 Collectors.joining(“,”),底层就是通过 StringJoiner 实现的。
源码如下:
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,CharSequence prefix,CharSequence suffix) {
return new CollectorImpl<>(() -> new StringJoiner(delimiter, prefix, suffix),
StringJoiner::add, StringJoiner::merge,
StringJoiner::toString, CH_NOID);
}
String 类的罕用办法有哪些?
- indexOf():返回指定字符的索引。
- charAt():返回指定索引处的字符。
- replace():字符串替换。
- trim():去除字符串两端空白。
- split():宰割字符串,返回一个宰割后的字符串数组。
- getBytes():返回字符串的 byte 类型数组。
- length():返回字符串长度。
- toLowerCase():将字符串转成小写字母。
- toUpperCase():将字符串转成大写字符。
- substring():截取字符串。
- equals():字符串比拟。
new String(“dabin”)会创立几个对象?
应用这种形式会创立两个字符串对象(前提是字符串常量池中没有 “dabin” 这个字符串对象)。
- “dabin” 属于字符串字面量,因而编译期间会在字符串常量池中创立一个字符串对象,指向这个 “dabin” 字符串字面量;
- 应用 new 的形式会在堆中创立一个字符串对象。
什么是字符串常量池?
字符串常量池(String Pool)保留着所有字符串字面量,这些字面量在编译期间就确定。字符串常量池位于堆内存中,专门用来存储字符串常量。在创立字符串时,JVM 首先会查看字符串常量池,如果该字符串曾经存在池中,则返回其援用,如果不存在,则创立此字符串并放入池中,并返回其援用。
String 最大长度是多少?
String 类提供了一个 length 办法,返回值为 int 类型,而 int 的取值下限为 2^31 -1。
所以实践上 String 的最大长度为 2^31 -1。
达到这个长度的话须要多大的内存吗?
String 外部是应用一个 char 数组来保护字符序列的,一个 char 占用两个字节。如果说 String 最大长度是 2^31 - 1 的话,那么最大的字符串占用内存空间约等于 4GB。
也就是说,咱们须要有大于 4GB 的 JVM 运行内存才行。
那 String 个别都存储在 JVM 的哪块区域呢?
字符串在 JVM 中的存储分两种状况,一种是 String 对象,存储在 JVM 的堆栈中。一种是字符串常量,存储在常量池外面。
什么状况下字符串会存储在常量池呢?
当通过字面量进行字符串申明时,比方 String s = “ 程序新大彬 ”;,这个字符串在编译之后会以常量的模式进入到常量池。
那常量池中的字符串最大长度是 2^31- 1 吗?
不是的,常量池对 String 的长度是有另外限度的。。Java 中的 UTF- 8 编码的 Unicode 字符串在常量池中以 CONSTANT_Utf8 类型示意。
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
length 在这里就是代表字符串的长度,length 的类型是 u2,u2 是无符号的 16 位整数,也就是说最大长度能够做到 2^16-1 即 65535。
不过 javac 编译器做了限度,须要 length < 65535。所以字符串常量在常量池中的最大长度是 65535 – 1 = 65534。
最初总结一下:
String 在不同的状态下,具备不同的长度限度。
- 字符串常量长度不能超过 65534
- 堆内字符串的长度不超过 2^31-1
Object 罕用办法有哪些?
Java 面试常常会呈现的一道题目,Object 的罕用办法。上面给大家整顿一下。
Object 罕用办法有:toString()
、equals()
、hashCode()
、clone()
等。
toString
默认输入对象地址。
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public static void main(String[] args) {System.out.println(new Person(18, "程序员大彬").toString());
}
//output
//me.tyson.java.core.Person@4554617c
}
能够重写 toString 办法,依照重写逻辑输入对象值。
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {return name + ":" + age;}
public static void main(String[] args) {System.out.println(new Person(18, "程序员大彬").toString());
}
//output
// 程序员大彬:18
}
equals
默认比拟两个援用变量是否指向同一个对象(内存地址)。
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public static void main(String[] args) {
String name = "程序员大彬";
Person p1 = new Person(18, name);
Person p2 = new Person(18, name);
System.out.println(p1.equals(p2));
}
//output
//false
}
能够重写 equals 办法,依照 age 和 name 是否相等来判断:
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {if (o instanceof Person) {Person p = (Person) o;
return age == p.age && name.equals(p.name);
}
return false;
}
public static void main(String[] args) {
String name = "程序员大彬";
Person p1 = new Person(18, name);
Person p2 = new Person(18, name);
System.out.println(p1.equals(p2));
}
//output
//true
}
hashCode
将与对象相干的信息映射成一个哈希值,默认的实现 hashCode 值是依据内存地址换算进去。
public class Cat {public static void main(String[] args) {System.out.println(new Cat().hashCode());
}
//out
//1349277854
}
clone
Java 赋值是复制对象援用,如果咱们想要失去一个对象的正本,应用赋值操作是无奈达到目标的。Object 对象有个 clone()办法,实现了对
象中各个属性的复制,但它的可见范畴是 protected 的。
protected native Object clone() throws CloneNotSupportedException;
所以实体类应用克隆的前提是:
- 实现 Cloneable 接口,这是一个标记接口,本身没有办法,这应该是一种约定。调用 clone 办法时,会判断有没有实现 Cloneable 接口,没有实现 Cloneable 的话会抛异样 CloneNotSupportedException。
- 笼罩 clone()办法,可见性晋升为 public。
public class Cat implements Cloneable {
private String name;
@Override
protected Object clone() throws CloneNotSupportedException {return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {Cat c = new Cat();
c.name = "程序员大彬";
Cat cloneCat = (Cat) c.clone();
c.name = "大彬";
System.out.println(cloneCat.name);
}
//output
// 程序员大彬
}
getClass
返回此 Object 的运行时类,罕用于 java 反射机制。
public class Person {
private String name;
public Person(String name) {this.name = name;}
public static void main(String[] args) {Person p = new Person("程序员大彬");
Class clz = p.getClass();
System.out.println(clz);
// 获取类名
System.out.println(clz.getName());
}
/**
* class com.tyson.basic.Person
* com.tyson.basic.Person
*/
}
wait
以后线程调用对象的 wait()办法之后,以后线程会开释对象锁,进入期待状态。期待其余线程调用此对象的 notify()/notifyAll()唤醒或者期待超时工夫 wait(long timeout)主动唤醒。线程须要获取 obj 对象锁之后能力调用 obj.wait()。
notify
obj.notify()唤醒在此对象上期待的单个线程,抉择是任意性的。notifyAll()唤醒在此对象上期待的所有线程。
讲讲深拷贝和浅拷贝?
浅拷贝:拷⻉对象和原始对象的引⽤类型援用同⼀个对象。
以下例子,Cat 对象外面有个 Person 对象,调用 clone 之后,克隆对象和原对象的 Person 援用的是同一个对象,这就是浅拷贝。
public class Cat implements Cloneable {
private String name;
private Person owner;
@Override
protected Object clone() throws CloneNotSupportedException {return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {Cat c = new Cat();
Person p = new Person(18, "程序员大彬");
c.owner = p;
Cat cloneCat = (Cat) c.clone();
p.setName("大彬");
System.out.println(cloneCat.owner.getName());
}
//output
// 大彬
}
深拷贝:拷贝对象和原始对象的援用类型援用不同的对象。
以下例子,在 clone 函数中不仅调用了 super.clone,而且调用 Person 对象的 clone 办法(Person 也要实现 Cloneable 接口并重写 clone 办法),从而实现了深拷贝。能够看到,拷贝对象的值不会受到原对象的影响。
public class Cat implements Cloneable {
private String name;
private Person owner;
@Override
protected Object clone() throws CloneNotSupportedException {
Cat c = null;
c = (Cat) super.clone();
c.owner = (Person) owner.clone();// 拷贝 Person 对象
return c;
}
public static void main(String[] args) throws CloneNotSupportedException {Cat c = new Cat();
Person p = new Person(18, "程序员大彬");
c.owner = p;
Cat cloneCat = (Cat) c.clone();
p.setName("大彬");
System.out.println(cloneCat.owner.getName());
}
//output
// 程序员大彬
}
两个对象的 hashCode()雷同,则 equals()是否也肯定为 true?
equals 与 hashcode 的关系:
- 如果两个对象调用 equals 比拟返回 true,那么它们的 hashCode 值肯定要雷同;
- 如果两个对象的 hashCode 雷同,它们并不一定雷同。
hashcode 办法次要是用来 晋升对象比拟的效率 ,先进行 hashcode() 的比拟,如果不雷同,那就不用在进行 equals 的比拟,这样就大大减少了 equals 比拟的次数,当比拟对象的数量很大的时候能晋升效率。
为什么重写 equals 时肯定要重写 hashCode?
之所以重写 equals()
要重写 hashcode()
,是为了保障equals()
办法返回 true 的状况下 hashcode 值也要统一,如果重写了 equals()
没有重写 hashcode()
,就会呈现两个对象相等但hashcode()
不相等的状况。这样,当用其中的一个对象作为键保留到 hashMap、hashTable 或 hashSet 中,再以另一个对象作为键值去查找他们的时候,则会查找不到。
Java 创建对象有几种形式?
Java 创建对象有以下几种形式:
- 用 new 语句创建对象。
- 应用反射,应用 Class.newInstance()创建对象。
- 调用对象的 clone()办法。
- 使用反序列化伎俩,调用 java.io.ObjectInputStream 对象的 readObject()办法。
说说类实例化的程序
Java 中类实例化程序:
- 动态属性,动态代码块。
- 一般属性,一般代码块。
- 构造方法。
public class LifeCycle {
// 动态属性
private static String staticField = getStaticField();
// 动态代码块
static {System.out.println(staticField);
System.out.println("动态代码块初始化");
}
// 一般属性
private String field = getField();
// 一般代码块
{System.out.println(field);
System.out.println("一般代码块初始化");
}
// 构造方法
public LifeCycle() {System.out.println("构造方法初始化");
}
// 静态方法
public static String getStaticField() {
String statiFiled = "动态属性初始化";
return statiFiled;
}
// 一般办法
public String getField() {
String filed = "一般属性初始化";
return filed;
}
public static void main(String[] argc) {new LifeCycle();
}
/**
* 动态属性初始化
* 动态代码块初始化
* 一般属性初始化
* 一般代码块初始化
* 构造方法初始化
*/
}
equals 和 == 有什么区别?
- 对于根本数据类型,== 比拟的是他们的值。根本数据类型没有 equal 办法;
- 对于复合数据类型,== 比拟的是它们的寄存地址 (是否是同一个对象)。
equals()
默认比拟地址值,重写的话依照重写逻辑去比拟。
常见的关键字有哪些?
static
static 能够用来润饰类的成员办法、类的成员变量。
static 变量也称作 动态变量,动态变量和非动态变量的区别是:动态变量被所有的对象所共享,在内存中只有一个正本,它当且仅当在类首次加载时会被初始化。而非动态变量是对象所领有的,在创建对象的时候被初始化,存在多个正本,各个对象领有的正本互不影响。
以下例子,age 为非动态变量,则 p1 打印后果是:Name:zhangsan, Age:10
;若 age 应用 static 润饰,则 p1 打印后果是:Name:zhangsan, Age:12
,因为 static 变量在内存只有一个正本。
public class Person {
String name;
int age;
public String toString() {return "Name:" + name + ", Age:" + age;}
public static void main(String[] args) {Person p1 = new Person();
p1.name = "zhangsan";
p1.age = 10;
Person p2 = new Person();
p2.name = "lisi";
p2.age = 12;
System.out.println(p1);
System.out.println(p2);
}
/**Output
* Name:zhangsan, Age:10
* Name:lisi, Age:12
*///~
}
static 办法个别称作 静态方法。静态方法不依赖于任何对象就能够进行拜访,通过类名即可调用静态方法。
public class Utils {public static void print(String s) {System.out.println("hello world:" + s);
}
public static void main(String[] args) {Utils.print("程序员大彬");
}
}
动态代码块 只会在类加载的时候执行一次。以下例子,startDate 和 endDate 在类加载的时候进行赋值。
class Person {
private Date birthDate;
private static Date startDate, endDate;
static{startDate = Date.valueOf("2008");
endDate = Date.valueOf("2021");
}
public Person(Date birthDate) {this.birthDate = birthDate;}
}
动态外部类
在静态方法里,应用⾮动态外部类依赖于外部类的实例,也就是说须要先创立外部类实例,能力用这个实例去创立非动态外部类。⽽动态外部类不须要。
public class OuterClass {class InnerClass {}
static class StaticInnerClass { }
public static void main(String[] args) {
// 在静态方法里,不能间接应用 OuterClass.this 去创立 InnerClass 的实例
// 须要先创立 OuterClass 的实例 o,而后通过 o 创立 InnerClass 的实例
// InnerClass innerClass = new InnerClass();
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
StaticInnerClass staticInnerClass = new StaticInnerClass();
outerClass.test();}
public void nonStaticMethod() {InnerClass innerClass = new InnerClass();
System.out.println("nonStaticMethod...");
}
}
final
- 根本数据 类型用 final 润饰,则不能批改,是常量;对象援用 用 final 润饰,则援用只能指向该对象,不能指向别的对象,然而对象自身能够批改。
- final 润饰的办法不能被子类重写
- final 润饰的类不能被继承。
this
this. 属性名称
指拜访类中的成员变量,能够用来辨别成员变量和局部变量。如下代码所示,this.name
拜访类 Person 以后实例的变量。
/**
* @description:
* @author: 程序员大彬
* @time: 2021-08-17 00:29
*/
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
this. 办法名称
用来拜访本类的办法。以下代码中,this.born()
调用类 Person 的以后实例的办法。
/**
* @description:
* @author: 程序员大彬
* @time: 2021-08-17 00:29
*/
public class Person {
String name;
int age;
public Person(String name, int age) {this.born();
this.name = name;
this.age = age;
}
void born() {}
}
super
super 关键字用于在子类中拜访父类的变量和办法。
class A {
protected String name = "大彬";
public void getName() {System.out.println("父类:" + name);
}
}
public class B extends A {
@Override
public void getName() {System.out.println(super.name);
super.getName();}
public static void main(String[] args) {B b = new B();
b.getName();}
/**
* 大彬
* 父类: 大彬
*/
}
在子类 B 中,咱们重写了父类的 getName()
办法,如果在重写的 getName()
办法中咱们要调用父类的雷同办法,必须要通过 super 关键字显式指出。
final, finally, finalize 的区别
- final 用于润饰属性、办法和类, 别离示意属性不能被从新赋值,办法不可被笼罩,类不可被继承。
- finally 是异样解决语句构造的一部分,个别以
try-catch-finally
呈现,finally
代码块示意总是被执行。 - finalize 是 Object 类的一个办法,该办法个别由垃圾回收器来调用,当咱们调用
System.gc()
办法的时候,由垃圾回收器调用finalize()
办法,回收垃圾,JVM 并不保障此办法总被调用。
final 关键字的作用?
- final 润饰的类不能被继承。
- final 润饰的办法不能被重写。
- final 润饰的变量叫常量,常量必须初始化,初始化之后值就不能被批改。
办法重载和重写的区别?
同个类中的多个办法能够有雷同的办法名称,然而有不同的参数列表,这就称为办法重载。参数列表又叫参数签名,包含参数的类型、参数的个数、参数的程序,只有有一个不同就叫做参数列表不同。
重载是面向对象的一个根本个性。
public class OverrideTest {void setPerson() { }
void setPerson(String name) {//set name}
void setPerson(String name, int age) {//set name and age}
}
办法的重写形容的是父类和子类之间的。当父类的性能无奈满足子类的需要,能够在子类对办法进行重写。办法重写时,办法名与形参列表必须统一。
如下代码,Person 为父类,Student 为子类,在 Student 中重写了 dailyTask 办法。
public class Person {
private String name;
public void dailyTask() {System.out.println("work eat sleep");
}
}
public class Student extends Person {
@Override
public void dailyTask() {System.out.println("study eat sleep");
}
}
接口与抽象类区别?
1、语法层面 上的区别
- 抽象类能够有办法实现,而接口的办法中只能是形象办法(Java 8 之后接口办法能够有默认实现);
- 抽象类中的成员变量能够是各种类型的,接口中的成员变量只能是 public static final 类型;
- 接口中不能含有动态代码块以及静态方法,而抽象类能够有动态代码块和静态方法(Java 8 之后接口能够有静态方法);
- 一个类只能继承一个抽象类,而一个类却能够实现多个接口。
2、设计层面 上的区别
- 抽象层次不同。抽象类是对整个类整体进行形象,包含属性、行为,然而接口只是对类行为进行形象。继承抽象类是一种 ” 是不是 ” 的关系,而接口实现则是 “ 有没有 ” 的关系。如果一个类继承了某个抽象类,则子类必然是抽象类的品种,而接口实现则是具备不具备的关系,比方鸟是否能飞。
- 继承抽象类的是具备类似特点的类,而实现接口的却能够不同的类。
门和警报的例子:
class AlarmDoor extends Door implements Alarm {//code}
class BMWCar extends Car implements Alarm {//code}
常见的 Exception 有哪些?
常见的 RuntimeException:
ClassCastException
// 类型转换异样IndexOutOfBoundsException
// 数组越界异样NullPointerException
// 空指针ArrayStoreException
// 数组存储异样NumberFormatException
// 数字格式化异样ArithmeticException
// 数学运算异样
checked Exception:
NoSuchFieldException
// 反射异样,没有对应的字段ClassNotFoundException
// 类没有找到异样IllegalAccessException
// 平安权限异样,可能是反射时调用了 private 办法
Error 和 Exception 的区别?
Error:JVM 无奈解决的重大问题,如栈溢出 StackOverflowError
、内存溢出OOM
等。程序无奈解决的谬误。
Exception:其它因编程谬误或偶尔的外在因素导致的一般性问题。能够在代码中进行解决。如:空指针异样、数组下标越界等。
运行时异样和非运行时异样(checked)的区别?
unchecked exception
包含 RuntimeException
和Error
类,其余所有异样称为查看(checked)异样。
RuntimeException
由程序谬误导致,应该修改程序防止这类异样产生。checked Exception
由具体的环境(读取的文件不存在或文件为空或 sql 异样)导致的异样。必须进行解决,不然编译不通过,能够 catch 或者 throws。
throw 和 throws 的区别?
- throw:用于抛出一个具体的异样对象。
- throws:用在办法签名中,用于申明该办法可能抛出的异样。子类办法抛出的异样范畴更加小,或者基本不抛异样。
通过故事讲清楚 NIO
上面通过一个例子来解说下。
假如某银行只有 10 个职员。该银行的业务流程分为以下 4 个步骤:
1)顾客填申请表(5 分钟);
2)职员审核(1 分钟);
3)职员叫保安去金库取钱(3 分钟);
4)职员打印票据,并将钱和票据返回给顾客(1 分钟)。
上面咱们看看银行不同的工作形式对其工作效率到底有何影响。
首先是 BIO 形式。
每来一个顾客,马上由一位职员来接待解决,并且这个职员须要负责以上 4 个残缺流程。当超过 10 个顾客时,残余的顾客须要排队等待。
一个职员解决一个顾客须要 10 分钟(5+1+3+1)工夫。一个小时(60 分钟)能解决 6 个顾客,一共 10 个职员,那就是只能解决 60 个顾客。
能够看到银行职员的工作状态并不饱和,比方在第 1 步,其实是处于期待中。
这种工作其实就是 BIO,每次来一个申请(顾客),就调配到线程池中由一个线程(职员)解决,如果超出了线程池的最大下限(10 个),就扔到队列期待。
那么如何进步银行的吞吐量呢?
思路就是:分而治之,将工作拆分开来,由专门的人负责专门的工作。
具体来讲,银行专门指派一名职员 A,A 的工作就是每当有顾客到银行,他就递上表格让顾客填写。每当有顾客填好表后,A 就将其随机指派给残余的 9 名职员实现后续步骤。
这种形式下,假如顾客十分多,职员 A 的工作处于饱和中,他一直的将填好表的顾客带到柜台解决。
柜台一个职员 5 分钟能解决完一个顾客,一个小时 9 名职员能解决:9*(60/5)=108。
可见工作形式的转变能带来效率的极大晋升。
这种工作形式其实就 NIO 的思路。
下图是十分经典的 NIO 阐明图,mainReactor
线程负责监听 server socket,接管新连贯,并将建设的 socket 分派给subReactor
subReactor
能够是一个线程,也能够是线程池,负责多路拆散已连贯的 socket,读写网络数据。这里的读写网络数据可类比顾客填表这一耗时动作,对具体的业务解决性能,其扔给 worker 线程池实现
能够看到典型 NIO 有三类线程,别离是 mainReactor
线程、subReactor
线程、work
线程。
不同的线程干业余的事件,最终每个线程都没空着,零碎的吞吐量天然就下来了。
那这个流程还有没有什么能够进步的中央呢?
能够看到,在这个业务流程里边第 3 个步骤,职员叫保安去金库取钱(3 分钟)。这 3 分钟柜台职员是在期待中度过的,能够把这 3 分钟利用起来。
还是分而治之的思路,指派 1 个职员 B 来专门负责第 3 步骤。
每当柜台员工实现第 2 步时,就告诉职员 B 来负责与保安沟通取钱。这时候柜台员工能够持续解决下一个顾客。
当职员 B 拿到钱之后,告诉顾客钱曾经到柜台了,让顾客从新排队解决,当柜台职员再次服务该顾客时,发现该顾客前 3 步曾经实现,间接执行第 4 步即可。
在当今 web 服务中,常常须要通过 RPC 或者 Http 等形式调用第三方服务,这里对应的就是第 3 步,如果这步耗时较长,通过异步形式将能极大升高资源使用率。
NIO+ 异步的形式能让大量的线程做大量的事件。这实用于很多利用场景,比方代理服务、api 服务、长连贯服务等等。这些利用如果用同步形式将消耗大量机器资源。
不过尽管 NIO+ 异步能进步零碎吞吐量,但其并不能让一个申请的等待时间降落,相同可能会减少等待时间。
最初,NIO 根本思维总结起来就是:分而治之,将工作拆分开来,由专门的人负责专门的工作
BIO/NIO/AIO 区别的区别?
同步阻塞 IO : 用户过程发动一个 IO 操作当前,必须期待 IO 操作的真正实现后,能力持续运行。
同步非阻塞 IO: 客户端与服务器通过 Channel 连贯,采纳多路复用器轮询注册的Channel
。进步吞吐量和可靠性。用户过程发动一个 IO 操作当前,可做其它事件,但用户过程须要轮询 IO 操作是否实现,这样造成不必要的 CPU 资源节约。
异步非阻塞 IO: 非阻塞异步通信模式,NIO 的升级版,采纳异步通道实现异步通信,其 read 和 write 办法均是异步办法。用户过程发动一个 IO 操作,而后立刻返回,等 IO 操作真正的实现当前,应用程序会失去 IO 操作实现的告诉。相似 Future 模式。
守护线程是什么?
- 守护线程是运行在后盾的一种非凡过程。
- 它独立于管制终端并且周期性地执行某种工作或期待解决某些产生的事件。
- 在 Java 中垃圾回收线程就是非凡的守护线程。
Java 反对多继承吗?
java 中,类不反对 多继承。接口才反对 多继承。接口的作用是拓展对象性能。当一个子接口继承了多个父接口时,阐明子接口拓展了多个性能。当一个类实现该接口时,就拓展了多个的性能。
Java 不反对多继承的起因:
- 出于安全性的思考,如果子类继承的多个父类外面有雷同的办法或者属性,子类将不晓得具体要继承哪个。
- Java 提供了接口和外部类以达到实现多继承性能,补救单继承的缺点。
如何实现对象克隆?
- 实现
Cloneable
接口,重写clone()
办法。这种形式是浅拷贝,即如果类中属性有自定义援用类型,只拷贝援用,不拷贝援用指向的对象。如果对象的属性的 Class 也实现Cloneable
接口,那么在克隆对象时也会克隆属性,即深拷贝。 - 联合序列化,深拷贝。
- 通过
org.apache.commons
中的工具类BeanUtils
和PropertyUtils
进行对象复制。
同步和异步的区别?
同步:收回一个调用时,在没有失去后果之前,该调用就不返回。
异步:在调用收回后,被调用者返回后果之后会告诉调用者,或通过回调函数解决这个调用。
阻塞和非阻塞的区别?
阻塞和非阻塞关注的是线程的状态。
阻塞调用是指调用后果返回之前,以后线程会被挂起。调用线程只有在失去后果之后才会复原运行。
非阻塞调用指在不能立即失去后果之前,该调用不会阻塞以后线程。
举个例子,了解下同步、阻塞、异步、非阻塞的区别:
同步就是烧开水,要本人来看开没开;异步就是水开了,而后水壶响了告诉你水开了(回调告诉)。阻塞是烧开水的过程中,你不能干其余事件,必须在旁边等着;非阻塞是烧开水的过程里能够干其余事件。
Java8 的新个性有哪些?
- Lambda 表达式:Lambda 容许把函数作为一个办法的参数
- Stream API:新增加的 Stream API(java.util.stream)把真正的函数式编程格调引入到 Java 中
- 默认办法:默认办法就是一个在接口外面有了一个实现的办法。
- Optional 类:Optional 类曾经成为 Java 8 类库的一部分,用来解决空指针异样。
- Date Time API:增强对日期与工夫的解决。
Java8 新个性总结
序列化和反序列化
- 序列化:把对象转换为字节序列的过程称为对象的序列化.
- 反序列化:把字节序列复原为对象的过程称为对象的反序列化.
什么时候须要用到序列化和反序列化呢?
当咱们只在本地 JVM 里运行下 Java 实例,这个时候是不须要什么序列化和反序列化的,但当咱们须要将内存中的对象长久化到磁盘,数据库中时,当咱们须要与浏览器进行交互时,当咱们须要实现 RPC 时,这个时候就须要序列化和反序列化了.
前两个须要用到序列化和反序列化的场景,是不是让咱们有一个很大的疑难? 咱们在与浏览器交互时,还有将内存中的对象长久化到数据库中时,如同都没有去进行序列化和反序列化,因为咱们都没有实现 Serializable 接口,但始终失常运行.
上面先给出论断:
只有咱们对内存中的对象进行长久化或网络传输,这个时候都须要序列化和反序列化.
理由:
服务器与浏览器交互时真的没有用到 Serializable 接口吗? JSON 格局实际上就是将一个对象转化为字符串,所以服务器与浏览器交互时的数据格式其实是字符串,咱们来看来 String 类型的源码:
public final class String
implements java.io.Serializable,Comparable<String>,CharSequence {
/\*\* The value is used for character storage. \*/
private final char value\[\];
/\*\* Cache the hash code for the string \*/
private int hash; // Default to 0
/\*\* use serialVersionUID from JDK 1.0.2 for interoperability \*/
private static final long serialVersionUID = -6849794470754667710L;
......
}
String 类型实现了 Serializable 接口,并显示指定 serialVersionUID 的值.
而后咱们再来看对象长久化到数据库中时的状况,Mybatis 数据库映射文件里的 insert 代码:
<insert id="insertUser" parameterType="org.tyshawn.bean.User">
INSERT INTO t\_user(name,age) VALUES (#{name},#{age})
</insert>
实际上咱们并不是将整个对象长久化到数据库中,而是将对象中的属性长久化到数据库中,而这些属性(如 Date/String)都实现了 Serializable 接口。
实现序列化和反序列化为什么要实现 Serializable 接口?
在 Java 中实现了 Serializable 接口后,JVM 在类加载的时候就会发现咱们实现了这个接口,而后在初始化实例对象的时候就会在底层帮咱们实现序列化和反序列化。
如果被写对象类型不是 String、数组、Enum,并且没有实现 Serializable 接口,那么在进行序列化的时候,将抛出 NotSerializableException。源码如下:
// remaining cases
if (obj instanceof String) {writeString((String) obj, unshared);
} else if (cl.isArray()) {writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {writeOrdinaryObject(obj, desc, unshared);
} else {if (extendedDebugInfo) {
throw new NotSerializableException(cl.getName() + "\n" + debugInfoStack.toString());
} else {throw new NotSerializableException(cl.getName());
}
}
实现 Serializable 接口之后,为什么还要显示指定 serialVersionUID 的值?
如果不显示指定 serialVersionUID,JVM 在序列化时会依据属性主动生成一个 serialVersionUID,而后与属性一起序列化,再进行长久化或网络传输. 在反序列化时,JVM 会再依据属性主动生成一个新版 serialVersionUID,而后将这个新版 serialVersionUID 与序列化时生成的旧版 serialVersionUID 进行比拟,如果雷同则反序列化胜利,否则报错.
如果显示指定了 serialVersionUID,JVM 在序列化和反序列化时依然都会生成一个 serialVersionUID,但值为咱们显示指定的值,这样在反序列化时新旧版本的 serialVersionUID 就统一了.
如果咱们的类写完后不再批改,那么不指定 serialVersionUID,不会有问题,但这在理论开发中是不可能的,咱们的类会一直迭代,一旦类被批改了,那旧对象反序列化就会报错。所以在理论开发中,咱们都会显示指定一个 serialVersionUID。
static 属性为什么不会被序列化?
因为序列化是针对对象而言的,而 static 属性优先于对象存在,随着类的加载而加载,所以不会被序列化.
看到这个论断,是不是有人会问,serialVersionUID 也被 static 润饰,为什么 serialVersionUID 会被序列化? 其实 serialVersionUID 属性并没有被序列化,JVM 在序列化对象时会主动生成一个 serialVersionUID,而后将咱们显示指定的 serialVersionUID 属性值赋给主动生成的 serialVersionUID.
transient 关键字的作用?
Java 语言的关键字,变量修饰符,如果用 transient 申明一个实例变量,当对象存储时,它的值不须要维持。
也就是说被 transient 润饰的成员变量,在序列化的时候其值会被疏忽,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
什么是反射?
动静获取的信息以及动静调用对象的办法的性能称为 Java 语言的反射机制。
在运行状态中,对于任意一个类,可能晓得这个类的所有属性和办法。对于任意一个对象,可能调用它的任意一个办法和属性。
反射有哪些利用场景呢?
- JDBC 连贯数据库时应用
Class.forName()
通过反射加载数据库的驱动程序 - Eclispe、IDEA 等开发工具利用反射动静解析对象的类型与构造,动静提醒对象的属性和办法
- Web 服务器中利用反射调用了 Sevlet 的
service
办法 - JDK 动静代理底层依赖反射实现
讲讲什么是泛型?
Java 泛型是 JDK 5 中引⼊的⼀个新个性,容许在定义类和接口的时候使⽤类型参数。申明的类型参数在使⽤时⽤具体的类型来替换。
泛型最⼤的益处是能够提⾼代码的复⽤性。以 List 接口为例,咱们能够将 String、Integer 等类型放⼊ List 中,如不⽤泛型,寄存 String 类型要写⼀个 List 接口,寄存 Integer 要写另外⼀个 List 接口,泛型能够很好的解决这个问题。
如何进行一个正在运行的线程?
有几种形式。
1、应用线程的 stop 办法。
应用 stop()办法能够强制终止线程。不过 stop 是一个被废除掉的办法,不举荐应用。
应用 Stop 办法,会始终向上流传 ThreadDeath 异样,从而使得指标线程解锁所有锁住的监视器,即开释掉所有的对象锁。使得之前被锁住的对象得不到同步的解决,因而可能会造成数据不统一的问题。
2、应用 interrupt 办法中断线程,该办法只是通知线程要终止,但最终何时终止取决于计算机。调用 interrupt 办法仅仅是在以后线程中打了一个进行的标记,并不是真的进行线程。
接着调用 Thread.currentThread().isInterrupted()办法,能够用来判断以后线程是否被终止,通过这个判断咱们能够做一些业务逻辑解决,通常如果 isInterrupted 返回 true 的话,会抛一个中断异样,而后通过 try-catch 捕捉。
3、设置标记位
设置标记位,当标识位为某个值时,使线程失常退出。设置标记位是用到了共享变量的形式,为了保障共享变量在内存中的可见性,能够应用 volatile 润饰它,这样的话,变量取值始终会从主存中获取最新值。
然而这种 volatile 标记共享变量的形式,在线程产生阻塞时是无奈实现响应的。比方调用 Thread.sleep() 办法之后,线程处于不可运行状态,即使是主线程批改了共享变量的值,该线程此时根本无法查看循环标记,所以也就无奈实现线程中断。
因而,interrupt() 加上手动抛异样的形式是目前中断一个正在运行的线程 最为正确 的形式了。
什么是跨域?
简略来讲,跨域是指从一个域名的网页去申请另一个域名的资源。因为有 同源策略 的关系,个别是不容许这么间接拜访的。然而,很多场景常常会有跨域拜访的需要,比方,在前后端拆散的模式下,前后端的域名是不统一的,此时就会产生跨域问题。
那什么是同源策略呢?
所谓同源是指 ” 协定 + 域名 + 端口 ” 三者雷同,即使两个不同的域名指向同一个 ip 地址,也非同源。
同源策略限度以下几种行为:
1. Cookie、LocalStorage 和 IndexDB 无奈读取
2. DOM 和 Js 对象无奈取得
3. AJAX 申请不能发送
为什么要有同源策略?
举个例子,如果你刚刚在网银输出账号密码,查看了本人的余额,而后再去拜访其余带色彩的网站,这个网站能够拜访刚刚的网银站点,并且获取账号密码,那结果可想而知。因而,从平安的角度来讲,同源策略是有利于爱护网站信息的。
跨域问题怎么解决呢?
嗯,有以下几种办法:
CORS,跨域资源共享
CORS(Cross-origin resource sharing),跨域资源共享。CORS 其实是浏览器制订的一个标准,浏览器会主动进行 CORS 通信,它的实现次要在服务端,通过一些 HTTP Header 来限度能够拜访的域,例如页面 A 须要拜访 B 服务器上的数据,如果 B 服务器 上申明了容许 A 的域名拜访,那么从 A 到 B 的跨域申请就能够实现。
@CrossOrigin 注解
如果我的项目应用的是 Springboot,能够在 Controller 类上增加一个 @CrossOrigin(origins =”*”) 注解就能够实现对以后 controller 的跨域拜访了,当然这个标签也能够加到办法上,或者间接加到入口类上对所有接口进行跨域解决。留神 SpringMVC 的版本要在 4.2 或以上版本才反对 @CrossOrigin。
nginx 反向代理接口跨域
nginx 反向代理跨域原理如下:首先同源策略是浏览器的安全策略,不是 HTTP 协定的一部分。服务器端调用 HTTP 接口只是应用 HTTP 协定,不会执行 JS 脚本,不须要同源策略,也就不存在逾越问题。
nginx 反向代理接口跨域实现思路如下:通过 nginx 配置一个代理服务器(域名与 domain1 雷同,端口不同)做跳板机,反向代理拜访 domain2 接口,并且能够顺便批改 cookie 中 domain 信息,不便以后域 cookie 写入,实现跨域登录。
// proxy 服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #批改 cookie 里域名
index index.html index.htm;
add_header Access-Control-Allow-Origin http://www.domain1.com;
}
}
这样咱们的前端代理只有拜访 http:www.domain1.com:81/* 就能够了。
通过 jsonp 跨域
通常为了加重 web 服务器的负载,咱们把 js、css,img 等动态资源拆散到另一台独立域名的服务器上,在 html 页面中再通过相应的标签从不同域名下加载动态资源,这是浏览器容许的操作,基于此原理,咱们能够通过动态创建 script,再申请一个带参网址实现跨域通信。
设计接口要留神什么?
- 接口参数校验。接口必须校验参数,比方入参是否容许为空,入参长度是否合乎预期。
- 设计接口时,充分考虑接口的 可扩展性。思考接口是否能够复用,怎么放弃接口的可扩展性。
- 串行调用思考改并行调用。比方设计一个商城首页接口,须要查商品信息、营销信息、用户信息等等。如果是串行一个一个查,那耗时就比拟大了。这种场景是能够改为并行调用的,升高接口耗时。
- 接口是否须要 防重 解决。波及到数据库批改的,要思考防重解决,能够应用数据库防重表,以惟一流水号作为惟一索引。
- 日志打印全面,入参出参,接口耗时,记录好日志,不便甩锅。
- 批改旧接口时,留神 兼容性设计。
- 异样解决切当。应用 finally 敞开流资源、应用 log 打印而不是 e.printStackTrace()、不要吞异样等等
- 是否须要思考 限流。限流为了爱护零碎,避免流量洪峰超过零碎的承载能力。
过滤器和拦截器有什么区别?
1、实现原理不同。
过滤器和拦截器底层实现不同。过滤器是基于函数回调的,拦截器是基于 Java 的反射机制(动静代理)实现的。个别自定义的过滤器中都会实现一个 doFilter()办法,这个办法有一个 FilterChain 参数,而实际上它是一个回调接口。
2、应用范畴不同。
过滤器实现的是 javax.servlet.Filter 接口,而这个接口是在 Servlet 标准中定义的,也就是说过滤器 Filter 的应用要依赖于 Tomcat 等容器,导致它只能在 web 程序中应用。而拦截器是一个 Spring 组件,并由 Spring 容器治理,并不依赖 Tomcat 等容器,是能够独自应用的。拦截器不仅能利用在 web 程序中,也能够用于 Application、Swing 等程序中。
3、应用的场景不同。
因为拦截器更靠近业务零碎,所以拦截器次要用来实现我的项目中的业务判断的,比方:日志记录、权限判断等业务。而过滤器通常是用来实现通用性能过滤的,比方:敏感词过滤、响应数据压缩等性能。
4、触发机会不同。
过滤器 Filter 是在申请进入容器后,但在进入 servlet 之前进行预处理,申请完结是在 servlet 解决完当前。
拦截器 Interceptor 是在申请进入 servlet 后,在进入 Controller 之前进行预处理的,Controller 中渲染了对应的视图之后申请完结。
5、拦挡的申请范畴不同。
申请的执行程序是:申请进入容器 -> 进入过滤器 -> 进入 Servlet -> 进入拦截器 -> 执行控制器。能够看到过滤器和拦截器的执行机会也是不同的,过滤器会先执行,而后才会执行拦截器,最初才会进入真正的要调用的办法。
参考链接:https://segmentfault.com/a/1190000022833940
对接第三方接口要思考什么?
嗯,须要思考以下几点:
- 确认接口对接的 网络协议,是 https/http 或者自定义的公有协定等。
- 约定好 数据传参、响应格局(如 application/json),弱类型对接强类型语言时要特地留神
- 接口平安 方面,要确定身份校验形式,应用 token、证书校验等
- 确认是否须要接口调用失败后的 重试 机制,保障数据传输的最终一致性。
- 日志记录要全面。接口出入参数,以及解析之后的参数值,都要用日志记录下来,不便定位问题(甩锅)。
参考:https://blog.csdn.net/gzt19881123/article/details/108791034
后端接口性能优化有哪些办法?
有以下这些办法:
1、优化索引 。给 where 条件的关键字段,或者order by
前面的排序字段,加索引。
2、优化 sql 语句。比方防止应用 select *、批量操作、防止深分页、晋升 group by 的效率等
3、防止大事务。应用 @Transactional 注解这种申明式事务的形式提供事务性能,容易造成大事务,引发其余的问题。应该防止在事务中一次性解决太多数据,将一些跟事务无关的逻辑放到事务里面执行。
4、异步解决。剥离主逻辑和副逻辑,副逻辑能够异步执行,异步写库。比方用户购买的商品发货了,须要发短信告诉,短信告诉是副流程,能够异步执行,免得影响主流程的执行。
5、升高锁粒度。在并发场景下,多个线程同时批改数据,造成数据不统一的状况。这种状况下,个别会加锁解决。但如果锁加得不好,导致锁的粒度太粗,也会十分影响接口性能。
6、加缓存 。如果表数据量十分大的话,间接从数据库查问数据,性能会十分差。能够应用 Redis 和
memcached 晋升查问性能,从而进步接口性能。
7、分库分表。当零碎倒退到肯定的阶段,用户并发量大,会有大量的数据库申请,须要占用大量的数据库连贯,同时会带来磁盘 IO 的性能瓶颈问题。或者数据库表数据十分大,SQL 查问即便走了索引,也很耗时。这时,能够通过分库分表解决。分库用于解决数据库连贯资源有余问题,和磁盘 IO 的性能瓶颈问题。分表用于解决单表数据量太大,sql 语句查问数据时,即便走了索引也十分耗时问题。
8、防止在循环中查询数据库。循环查询数据库,十分耗时,最好能在一次查问中获取所有须要的数据。
为什么在阿里巴巴 Java 开发手册中强制要求应用包装类型定义属性呢?
嗯,以布尔字段为例,当咱们没有设置对象的字段的值的时候,Boolean 类型的变量会设置默认值为null
,而 boolean 类型的变量会设置默认值为false
。
也就是说,包装类型的默认值都是 null,而根本数据类型的默认值是一个固定值,如 boolean 是 false,byte、short、int、long 是 0,float 是 0.0f 等。
举一个例子,比方有一个扣费零碎,扣费时须要从内部的定价零碎中读取一个费率的值,咱们预期该接口的返回值中会蕴含一个浮点型的费率字段。当咱们取到这个值得时候就应用公式:金额 * 费率 = 费用 进行计算,计算结果进行划扣。
如果因为计费零碎异样,他可能会返回个默认值,如果这个字段是 Double 类型的话,该默认值为 null,如果该字段是 double 类型的话,该默认值为 0.0。
如果扣费零碎对于该费率返回值没做非凡解决的话,拿到 null 值进行计算会间接报错,阻断程序。拿到 0.0 可能就间接进行计算,得出接口为 0 后进行扣费了。这种异常情况就无奈被感知。
那我能够对 0.0 做非凡判断,如果是 0 就阻断报错,这样是否能够呢?
不对,这时候就会产生一个问题,如果容许费率是 0 的场景又怎么解决呢?
应用根本数据类型只会让计划越来越简单,坑越来越多。
这种应用包装类型定义变量的形式,通过异样来阻断程序,进而能够被辨认到这种线上问题。如果应用根本数据类型的话,零碎可能不会报错,进而认为无异样。
因而,倡议在 POJO 和 RPC 的返回值中应用包装类型。
参考链接:https://mp.weixin.qq.com/s/O_jCxZWtTTkFZ9FlaZgOCg
8 招让接口性能晋升 100 倍
池化思维
如果你每次须要用到线程,都去创立,就会有减少肯定的耗时,而线程池能够反复利用线程,防止不必要的耗时。
比方 TCP
三次握手,它为了缩小性能损耗,引入了Keep-Alive 长连贯
,防止频繁的创立和销毁连贯。
回绝阻塞期待
如果你调用一个零碎 B
的接口,然而它解决业务逻辑,耗时须要 10s
甚至更多。而后你是始终 阻塞期待,直到零碎 B 的上游接口返回 ,再持续你的下一步操作吗?这样 显然不合理。
参考 IO 多路复用模型。即咱们不必阻塞期待零碎B
的接口,而是先去做别的操作。等零碎 B
的接口解决完,通过 事件回调 告诉,咱们接口收到告诉再进行对应的业务操作即可。
近程调用由串行改为并行
比方设计一个商城首页接口,须要查商品信息、营销信息、用户信息等等。如果是串行一个一个查,那耗时就比拟大了。这种场景是能够改为并行调用的,升高接口耗时。
锁粒度防止过粗
在高并发场景,为了避免 超卖等状况 ,咱们常常须要 加锁来爱护共享资源。然而,如果加锁的粒度过粗,是很影响接口性能的。
不论你是 synchronized
加锁还是 redis
分布式锁,只须要在共享临界资源加锁即可,不波及共享资源的,就不必要加锁。
耗时操作,思考放到异步执行
耗时操作,思考用 异步解决,这样能够升高接口耗时。比方用户注册胜利后,短信邮件告诉,是能够异步解决的。
应用缓存
把要查的数据,提前放好到缓存外面,须要时,间接查缓存,而防止去查数据库或者计算的过程。
提前初始化到缓存
预取思维很容易了解,就是 提前把要计算查问的数据,初始化到缓存 。如果你在将来某个工夫须要用到某个通过简单计算的数据, 才实时去计算的话,可能耗时比拟大 。这时候,咱们能够采取预取思维, 提前把未来可能须要的数据计算好,放到缓存中,等须要的时候,去缓存取就行。这将大幅度提高接口性能。
压缩传输内容
压缩传输内容,传输报文变得更小,因而传输会更快。
最初给大家分享一个 Github 仓库,下面有大彬整顿的 300 多本经典的计算机书籍 PDF,包含 C 语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生 等,能够 star 一下,下次找书间接在下面搜寻,仓库继续更新中~
Github 地址:https://github.com/Tyson0314/java-books