Java SE
0. 前言
以下内容,是我认为SE
部分比较有意思的地方 ,故特此编写。如果有其他重要的点被遗漏, 欢迎评论区补充,后期补充。
1. 面向过程和面向对象
面向对象和面向过程是一种思想,其并不是由编程语言所实现的。比如我们通常说,C
语言是面向过程的,Java
是面向对象的。难道C
语言就一定不能实现面向对象的思想吗,答案是否定的。
这里以我个人的观点阐述一下面向过程和面向对象的区别:
面向过程,通常是站在程序员即我们的角度去看待问题,是第一人称的视角,去一步步想我们应该怎么做才能解决问题。
面向对象,程序员则是第三人称,通过对需求进行抽象,抽象出一个个的对象,程序员负责将这些对象的相互协作来解决问题。但是每个对象去完成其工作依然属于面向过程。可以这么理解,面向对象在宏观上是多个对象相互合作,微观上依然是面向过程的。
了解了两者的思想,我们分析一下两者各自的优缺点。
面向过程的执行优点是速度更加快速,不需要先去创建对象,然后在执行方法。但是带来的问题便是,代码耦合性太高,不够清晰明了,当有新的需求时,需要修改原代码,很难符合开闭原则。
面向对象的优点则是代码耦合性较低,通过对象间的相互协作,直观明了,并且通过合适的设计模式,可以很容易达到开闭原则。问题则是,创建对象需要时间,执行速度没有面向过程快,并且对象需要占用一定的内存。
2. JDK、JRE 和 JVM 的区别
JDK
是Java
程序的开发包,而JRE
只是Java
程序的运行环境,JDK
中包含了JRE
。对于程序员来说,我们需要JDK
开发相关的程序,对于程序的使用者来说,仅需要JRE
即可。
并且整个Java
程序都是运行在JVM
之上,我们知道Java
语言是跨平台的,其跨平台的原因正是由于JVM
的存在,我们的Java
程序在Javac
编译后,会生成.class
文件,该程序一次编译,到处运行。.class
文件并不面向任何特定的处理器、操作系统,而只是JVM
。如此,即使我们在windows
下编译好的.class
文件,也可以运行在linux
环境下。
3. 基本数据类型和包装类型
基本数据类型 | 包装类型 | 字节 |
---|---|---|
boolean | Boolean | - |
byte | Byte | 1 |
short | Short | 2 |
char | Character | 2 |
int | Integer | 4 |
float | Float | 4 |
double | Double | 8 |
long | Long | 8 |
一个boolean
变量在编译后以一个int
代替,一个boolean
数组编译后,数组中每个成员是一个byte
。
Java
语言号称一切皆对象,但是基本数据类型并不属于对象,由此便产生了包装类型。包装类型的作用除了为了表示对象,还可以实现与基本数据类型的自动转换。
既然包装类型属于对象,那么其与基本数据类型也存在很大的区别:
- 初值不同,基本数据类型有其各自的数据类型,而包装类型的初值为
null
(这里的初值是成员变量而不是方法中的变量,方法中的变量必须赋初值才能使用); - 存储位置不同,基本类型存储位置在栈中,而包装类型的存储位置位于堆中(随着栈逃逸技术的出现,对象不在局限于出现在堆中);
- 泛型不能被指定为基本数据类型,只能为引用类型。
4. 自动拆装箱
自动装箱指的是,基本类型可以自动转换为包装类型,不需要通过new
关键字。
自动拆箱指的是,包装类型可以自动转换为基本数据类型,参与算术运算。
4.1 实现原理
自动装箱的实现原理:
public static void main(String[] args) {
Integer a = 1;
}
上述代码中,我们将一个基本数据类型直接赋值给了一个Integer
的类型,如此便实现了自动装箱。其编译后的字节码文件如下所示:
可以看到,编译器会自动调用Integer.valueOf()
方法,那么该方法的作用是什么?
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在该方法中,实现了new
的过程。
自动拆箱的实现原理:
public static void main(String[] args) {
Integer a = 1;
int b = a;
}
与自动拆箱一样,编译器同样会自动调用Integer.intValue()
方法,实现包装类型到基本类型的自动转换。
private final int value;
public int intValue() {
return value;
}
4.2 缓存池
我们知道对于基本类型==
的作用是比较两个变量的值,对于引用类型则是比较对象的地址,如果要比较对象的值则可以通过重写equal()
方法。
public static void main(String[] args) {
Integer a = 1;
Integer b = 1;
Integer c = new Integer(1);
System.out.println("a == b, " + (a == b ));
System.out.println("a == c, " + (a == c ));
}
该程序的运行结果如下:
首先可以明确的是,上述两个==
的作用比较的都是地址。但是为什么会出现不同的结果,答案正是由于缓存池的作用。
在包装类型中,缓存池的作用是在进行自动装箱时,直接从缓存池中获取对应的对象,否则则是在堆中创建对象。
我们回到自动装箱的代码中,如下:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
首先判断i
的值是否在指定的缓存池的范围内,如果在则返回缓存池中的引用,否则去创建对象。那么缓存池是什么?缓存池的大小是多大?
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;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
可以看到缓存池就是该包装类型的一个数组,其可表示的数据范围在-128,127
之间。
在介绍完缓存池后,那么是否是所有的包装类型都存在缓冲池,答案是否定的。
包装类型 | 是否存在缓存池 | 范围 |
---|---|---|
Boolean | 不存在,但是存在两个常量 | |
Byte | 存在 | [-128, 127] |
Short | 存在 | [-128, 127] |
Charcater | 存在 | [0, 127] |
Integer | 存在 | [-128,127] |
Float | 不存在 | |
Double | 不存在 | |
Long | 存在 | [-128,127] |
不知道你有没有写过这样的代码?将一个int
直接自动装箱为Double
。如果没有的话,可以去尝试一下,结果可能会出乎你的意料。
public static void main(String[] args) {
Double a = 1;
Double b = new Double(1);
}
如上所示,你可能会认为Double
自动装箱的话调vlaueOf(double d)
那么的话,1
可以自动类型转换为1.0
,那我当然可以实现自动装箱了,然而编译结果如下:
可以这样理解,自动装箱的过程中关闭了自动类型转换。
5. Object
面向对象的三大特征是:封装、继承、多态。
在Java
中,所有的类都是继承自Object
类,同时该类的方法在并发中也被经常使用到。
权限 | 方法名 | 作用 |
---|---|---|
public | getClass() | 获得Class对象 |
public | hashCode() | 获得哈希值 |
public | equals(Object obj) | 比较两个对象是否相等 |
public | toString() | 打印该对象的信息 |
public | notify() | 唤醒一个等待中的线程 |
public | notifyAll() | 唤醒多个等待中的线程 |
public | wait() | 让当前线程等待,并释放锁 |
public | wait(long timeout) | 让当前线程等待,直到指定时间或被唤醒 |
public | wait(long timeout, int nanos) | 同wait(long timeout),time = 1000000*timeout+nanos |
protected | clone() | 克隆对象 |
protected | finalize() | 被垃圾回收器清除之前执行 |
private | registerNatives() | 注册本地方法 |
通常,我们比较两个对象是否相等时,不仅会重写equal
方法,也会重写hashCode()
方法。这样做的意义是,hashCode
相同,两个对象的值不一定完全相同。但是hashCode
不同,两个对象的值一定不相同。
当我们往Set
、Map
中添加新元素时,需要先判断插入元素是否存在,首先根据hashCode()
方法得到该对象的hashCode
值,如果集合里边不存在该值,可以直接插入进去。如果已经存在,则需要再次通过equals()
来比较,这样的话可以提升效率。
6. 字符串
6.1 String
String
是经常被使用到的一个数据类型,需要注意的是,其是引用类型,并不是基本类型。
我们都知道String
是一个不可变字符串,那么为什么是不可变字符串?是因为String
数组内部封装了一个final
数组。
private final char value[];
我们知道对于被final
修饰的引用类型,只是其地址值不可以改变,但是其元素还是可以被改变的,那么还可以称为不可变字符串吗?
倘若str = "abc"
,那么则说明value = [‘a’, ‘b’, ‘c’]
,如果我们令str="abcd"
,此时是没办法实现的,因为value
不能指向一个新的数组,所以从这个角度来看,可以理解为不可变字符串,并且value
是由private
修饰外界且没有向外界提供访问的接口,更是无法直接修改value
数组。
存在如下一段代码:
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
输出如下:
true
false
这时因为每次new
都是在堆中分配一块内存,然后将该内存的引用返回。
如果字符串的常量池中已经存在对应的常量,直接返回对应的地址即可。
String s3 = new String(“abc”)
,因为字符串常量池已经存在对用的字符串常量,所以内存图如下:
在String
中,有一个方法即为有趣,即intern()
,该方法的作用是如果常量池中存在和目标串相同的值,则返回常量池中的地址。否则,直接返回引用,并且将目标串加入到字符串常量池中。
示例如下:
String s1 = new String("abc");
String s2 = s1.intern();
System.out.println(s1 == s2);
// 输出:false
String s1 = "abc";
String s2 = s1.intern();
System.out.println(s1 == s2);
// 输出:true
6.2 StringBuilder
在说到String
这个不可变字符串时,不得不说到可变字符串StringBuild
及StringBuffer
。因为我们有的时候并不想为了修改字符串而频繁的去创建String
对象,由此便有了可变字符串。
StringBuild
的源码如下所示:
public final class StringBuilderextends AbstractStringBuilderimplements java.io.Serializable, CharSequence{
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
可以看到其value
属性并不是由final
所修饰。
在我们创建StringBuilder
对象时,默认创建一个长度为16
的字符数组。
public StringBuilder() {
super(16);
}
6.3 StringBuffer
StringBuffer
与StringBuilder
的区别是StringBuffer
是线程安全的,其方法基本都是由synchronized
所修饰。
6.4 其他问题
不可变字符串和可变字符串各自的优缺点是什么?为什么要将String
设计成不可变字符串?
该部分内容转载自:String 为什么要设计成不可变?
不可变字符串的优点有:
- 便于实现字符串常量池
在Java
中,由于会大量的使用String
常量,如果每一次声明一个String
都创建一个String
对象,那将会造成极大的空间资源的浪费。Java
提出了String pool
的概念,在堆中开辟一块存储空间String pool
,当初始化一个String
变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
String a = "Hello world!";
String b = "Hello world!";
如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool
将不能够实现!
- 使多线程安全
有如下这个场景,一个函数appendStr()
在不可变的String
参数后面加上一段“bbb”
后返回。appendSb()
负责在可变的StringBuilder
后面加"bbb"
。
public class test {
// 不可变的String
public static String appendStr(String s) {
s += "bbb";
return s;
}
// 可变的StringBuilder
public static StringBuilder appendSb(StringBuilder sb) {
return sb.append("bbb");
}
public static void main(String[] args) {
String s = new String("aaa");
String ns = test.appendStr(s);
System.out.println("String aaa>>>" + s.toString());
// StringBuilder做参数
StringBuilder sb = new StringBuilder("aaa");
StringBuilder nsb = test.appendSb(sb);
System.out.println("StringBuilder aaa >>>" + sb.toString());
}
}
如果程序员不小心像上面例子里,直接在传进来的参数上加上“bbb”
。因为Java
对象参数传的是引用,所有可变的StringBuffer
参数就被改变了。可以看到变量sb
在Test.appendSb(sb)
操作之后,就变成了"aaabbb"
。
有的时候这可能不是程序员的本意。所以String
不可变的安全性就体现在这里。
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
- 避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL
,文件路径path
,反射机制所需要的String
参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
因为String
是不可变的,所以它的值是不可改变的。但由于String
不可变,也就没有任何方式能修改字符串的值,每一次修改都将产生新的字符串,如果使用char[]
来保存密码,仍然能够将其中所有的元素设置为空和清零,也不会被放入字符串缓存池中,用字符串数组来保存密码会更好。
- 加快字符串处理速度
由于String
是不可变的,保证了hashcode
的唯一性,于是在创建对象时其hashcode
就可以放心的缓存了,不需要重新计算。这也就是Map
喜欢将String
作为Key
的原因,处理速度要快过其它的键对象。所以HashMap
中的键往往都使用String
。
在String
类的定义中有如下代码:
private int hash;//用来缓存HashCode
总体来说,String
不可变的原因要包括 设计考虑,效率优化,以及安全性这三大方面。
不可变字符串的缺点有:
在优点中,我们已经说明了其缺点,便是每对String
对象做一次修改,都会重新创建一个对象,并放入到String Pool
中(如果String Pool
中不存在该对象),如果频繁的修改对象便造成大量空间的浪费。
而可变字符串的优点则正是用来弥补不可变字符串的缺点,不可变字符串的优点同样是用来弥补可变字符串的缺点。凡事都有两面性,所以我们需要在合适的场景选择合适的字符串。
7. 抽象类与接口
抽象类的作用往往是定义一个模板,然后其子类照着这个模板去实现相关模块,如前面说到的StringBuilder
,其便是继承自一个AbstractStringBuilder
。
接口的作用往往是定义有哪些行为,接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用。
抽象类与普通类的区别:
- 类名由
abstract
所修饰
public abstract class Person {
}
-
类名不能被
final
修饰 -
可以不包含方法的实现
public abstract class Person {
//如果方法被abstract修饰,说明这是一个抽象方法
public abstract void sayHello();
}
- 不能实例化
这里有一点值得注意,便是抽象类不能被实例化,那么其构造方法存在的意义是什么?
public abstract class Person {
public abstract void sayHello();
public Person(){
System.out.println("执行抽象类的构造方法");
}
}
class Student extends Person{
@Override
public void sayHello() {
System.out.println(getClass().getName()+":sayHello");
}
public Student(){
System.out.println("执行非抽象类的构造方法");
}
public static void main(String[] args) {
Student student = new Student();
student.sayHello();
}
}
上述代码的输入结果如下:
执行抽象类的构造方法
执行非抽象类的构造方法
Student:sayHello
可以看出,在执行子类的构造器时,会执行抽象类的构造方法,所以抽象类的构造方法可以完成一些初始化操作。
接口和抽象类的区别:
- 接口可视作比抽象类更加抽象的类。
- 接口中的方法只能为
public
,对于抽象方法而言,其只是不能被private
修饰。 - 接口没有构造方法。
- 接口中的变量默认被
public static final
所修饰。 - 在
JDK1.8
及以后,接口允许有方法体,因为当我们给接口拓展方法时,其每一个实现类都要重写该方法,这样会很不便。
public interface MyInterface {
void a();
//如果方法具有方法体,且非静态方法,必须被default修饰
default void b(){
System.out.println("hello interface");
}
}
8. 重载和重写
重载发生在同一个类中,表示方法名相同的情况下,允许参数列表的个数和类型不同。
interface Language{
}
class Java implements Language {
}
class Python implements Language {
}
class Go implements Language {
}
class Stu{
public void Study(Java java){
System.out.println("Study Java");
}
public void Study(Python python){
System.out.println("Study Python");
}
private void Study(Go go){
System.out.println("Study Go");
}
}
重写则是发生在父子类之间,子类可以重写父类的方法。
class Father{
public void print(){
System.out.println("Father");
}
}
class Son extends Father{
public void print(){
System.out.println("Son");
}
}
重载适用于同一个类的多个方法,这些方法功能是一致的,但细节可能不一致,如参数类型、个数彼此不同。
重写适用于父类方法不能满足子类的要求,但是子类要实现的功能和父类一致。
9. 多态
多态指的是一个对象可以有多种类型,分为编译时类型和运行时类型。编译时类型是在编译阶段就可以确定的,运行时则只能在程序运行的过程中确定。
public class Test {
public static void main(String[] args) {
Language language = null;
language = new Java();
language = new Python();
language = new Go();
}
}
interface Language{
}
class Java implements Language {
}
class Python implements Language {
}
class Go implements Language {
}
上述代码中,language
编译时类型为Language
,运行时类型则为Java
、Python
以及Go
。
多态下,成员访问的规则如下:
成员变量:编译看左边,运行看左边。
成员方法:编译看左边,运行看右边。
10. 泛型
可参考如下博客: