一个 Java 文件中是否可以存在多个类(修饰类除外)?
一个 Java 文件中是可以存在多个类的,但是一个 Java 文件中只能存在一个 public
所修饰的类,而且这个 Java 文件的文件名还必须和 public
所修饰类的类名保持一致,一个简单的实例如下。
public class Person{private String name;
}class Student{private Integer age;
}
Java 有哪些特点?
- 面向对象,具有封装、继承、多态三大特性
- 平台独立,Java 通过 Java 虚拟机运行字节码,所以无论在哪个平台中,一旦进行编译后,都可以在其他平台运行。
- 安全可靠
- 支持多线程
- 解释和编译共存
- 安全性
- 健壮性(Java 语言的强类型机制、异常处理、垃圾的自动收集等)
Java 和 C++ 的区别
- 相同点:两者均为 OOP(面向对象) 语言,均支持 OOP 的三大特性(封装、继承、多态)。
- 不同点:
- Java 不存在指针的概念,所以内存更加安全。
- Java 类是单继承(但是接口可以多继承),C++ 的类是多继承。
- Java 中有自动内存管理机制,但是 C++ 中需要开发者手动释放内存。
- C/C++ 中,字符串和字符数组最后均有一个额外的
\0
标志来表示结束,但 Java 中不存在这一概念。
JRE 和 JDK 有什么不同。
- JRE:Java Runtime Environment,即 Java 运行时环境,是用来运行已经编译过的 Java 程序所需内容的集合(JVM、Java 类库、Java 命令等),不能用来开发新程序。
- JDK:Java Development Kit,即 Java 开发工具包,是功能齐全的 Java SDK,包含 JRE 拥有的一切,还有编译器和其他工具,如果我们想要创建和编译新程序,就必须使用到它。
Java 程序编译过程
我们编译的源代码(xxx.java
)经 JDK 中的 javac
命令编译后,成为 JVM 能够理解的 Java 字节码(xxx.class
),然后经由 JVM 加载,通过解释器 逐行解释执行,这就是为什么能经常听见说 Java 是一门编译和解释共存的语言。
其中 JVM 是解释 Java 字节码(xxx.class
) 的虚拟机,针对不同系统均有特定实现,方便一次编译,多次运行,即 Java 语言的平台独立性;
说一下 Java 中的数据类型
Java 中,数据类型主要包括 8 大基本数据类型和引用数据类型两大类。
基本数据类型
数据类型 | bit | 字节 | 封装类 | 数据范围 | 默认值 |
---|---|---|---|---|---|
byte | 8 | 1 | Byte | − 2 7 2 7 − 1 -2^7 \text{~} 2^7-1 −27 27−1 | 0 |
short | 16 | 2 | Short | − 2 15 2 15 − 1 -2^{15} \text{~} 2^{15}-1 −215 215−1 | 0 |
char | 16 | 2 | Character | \\u0000 | |
~ \\uffff | |||||
( 0 65535 0 \text{~} 65535 0 65535) | u0000 | ||||
int | 32 | 4 | Integer | − 2 31 2 31 − 1 -2^{31} \text{~} 2^{31}-1 −231 231−1 | 0 |
long | 64 | 8 | Long | − 2 63 2 63 − 1 -2^{63} \text{~} 2^{63}-1 −263 263−1 | 0L |
float | 32 | 4 | Float | 3.4 e − 45 1.4 e 38 3.4e^{-45} \text{~} 1.4e^{38} 3.4e−45 1.4e38 | 0.0f |
double | 64 | 8 | Double | 4.9 e − 324 1.8 e 308 4.9e^{-324} \text{~} 1.8e^{308} 4.9e−324 1.8e308 | 0.0D |
boolean | 不确定 | 不确定 | Boolean | true | |
或 false | false |
注意:
boolean
一般用 1bit
来存储,但是具体大小并未规定,JVM 在编译期将boolean
类型转换为int
,此时 1 代表true
,0
代表false
。此外,JVM 还指出boolean
数组,但底层是通过byte
数组来实现;- 使用
long
类型时,需要在后边加上L
,否则将其作为整型解析,可能会导致越界; - 浮点数如果没有明确指定
float
还是double
,统一按double
处理; char
是用 **单引号 ****‘’**
将内容括起来,相当于一个整型值(ASCII 值),能够参加表达式运算;而String
是用 **双引号 **“”
将内容括起来,代表的是一个地址值;
引用类型
数据类型 | 默认值 |
---|---|
数组 | null |
类 | null |
接口 | null |
封装类
基本数据类型都有其对应的封装类,两者之间的赋值通过 自动装箱 和 自动拆箱 来完成;
- 自动装箱:将基本数据类型装箱为封装类;
// 实际调用 Integer.valueOf(12)
Integer x = 12;
- 自动拆箱:将封装类拆箱为基本数据类型;
Integer x = 12;
// 实际调用 x.intValue()
int y = x;
- 基本类型与对应封装类的不同
- 基本类型只能按值传递,封装类按引用传递;
- 基本类型 会在 栈 中创建,效率较高,但可能存在内存泄露问题;封装类对象会在堆中创建,其 引用在栈中创建;
缓存池
以 new Integer(123)
和 Integer.valueOf(123)
为例:
- 通过
new
的方式每次都会创建一个新的对象; - 通过
valueOf()
的方式则会优先判断该值是否位于缓存池,如果在的话就直接返回缓存池中的内容,多次调用指向同一个对象的引用;
Integer x = new Integer(123);
Integer y = new Integer(123);
// false,通过 new 的方式,每次都会创建一个新对象,指向不同对象
System.out.println(x == y);
Integer m = Integer.valueOf(123);
Integer n = Integer.valueOf(123);
// true,通过 valueOf() 的方式,先到缓存池中查找,存在时则多次调用也是指向同一对象
System.out.println(m == n);
数据类型 | 默认缓存池 |
---|---|
Byte | − 2 7 2 7 − 1 -2^7 \text{~} 2^7-1 −27 27−1 |
Character | \\u0000 |
~ \\u007F | |
Short | − 2 7 2 7 − 1 -2^7 \text{~} 2^7-1 −27 27−1 |
Integer | − 2 7 2 7 − 1 -2^7 \text{~} 2^7-1 −27 27−1 |
Boolean | true & false |
字符串 String
定义
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {/** The value is used for character storage. */private final char value[];
}
上述代码为 Java 8
中 String
的定义,其底层实际上使用的是字符(char
)数组,而且由于被声明为 final
,代表着它 不能被继承。而且一旦初始化之后就不能再去引用其他数组,这样就保证了 String
的不可变性,也因此 String 是线程安全的。
不可变性的优点
- 用于缓存
**hash**
值
由于 String
的 hash
值被频繁使用,它的不可变性使得 hash
值也不可变,此时只需要进行一次计算;
- 字符串常量池(String Pool)的需要
如果一个 String
对象已经被创建过,那么就会优先从字符串常量池中获取其引用,其不可变性确保了不同引用指向同一 String
对象;
- 安全性
我们经常用 String
作为我们方法的参数,其不变性能够保证参数不可变;
- 线程安全
String
的不可变性让它天生 具备线程安全,能够在多个线程中方便使用而不用考虑线程安全问题。
String vs StringBuffer vs StringBuffer
主要从三个方面对三者进行对比:
可变性 | 线程安全 | 适用场景 | |
---|---|---|---|
String | 不可变 | 安全 | 操作少量的数据 |
StringBuffer | 可变 | 安全,内部使用 synchronized 进行同步 | 多线程操作字符串缓冲区下操作大量数据 |
StringBuilder | 可变 | 不安全 | 单线程操作字符串缓冲区下操作大量数据,性能高于 StringBuffer |
字符串常量池(String Pool)
String Pool 位于 方法区,通常保存着所有 字符串字面量(literal strings),在编译期间就被确定。此外,还可以用 String
中的 intern()
方法在运行过程中添加到 String Pool
中。当一个字符串调用 intern()
时,如果 String Pool
中已经存在字面量相同的字符串,则会返回 String Pool
中的引用;如果不存在,则向 String Pool
中添加一个新的字符串,同时返回新字符串的引用。
String s1 = new String("aaa");
String s2 = new String("aaa");
// false 两个字符串指向不同对象
System.out.println(s1 == s2); String s3 = s1.intern();
String s4 = s1.intern();
// true,常量池中存在字面量相同的字符串,直接取出
System.out.println(s3 == s4);
在下面的代码中,内存分析如下图:
String str1 = "村";
String str2 = "村";
String str3 = new String("村");
String str4 = new String("村");// true,两个引用指向常量池中的同一对象
System.out.println(str1 == str2);
// false,两个引用指向堆中不同对象
System.out.println(str3 == str4);
new String(“xxx”)
使用 new
的方式创建字符串对象,会有两种不同的情况:
- String Pool 中不存在 “xxx”
此时会创建两个字符串对象,“xxx” 属于字符串字面量,因此在编译期会在 String Pool 中创建一个字符串对象,用于指向该字符串的字面量 “xxx”;然后 new
会在堆中创建一个字符串对象;
- String Pool 中存在 “xxx”
此时只需要创建一个字符串对象,由于 String Pool 中已经存在指向 “xxx” 的对象,所以直接在堆中创建一个字符串对象;
基础语法
注释
- 单行注释
// 这是单行注释
String name = "村";
- 多行注释
String name = "村";
- 文档注释
String name = "村";
常见关键字
标识符和关键字
- 标识符:用于给程序、类、对象、变量、方法、接口、自定义数据类型等命名;
- 关键字:特殊的标识符,被 Java 赋予了特殊含义,只能有特定用途;
- 标识符命名规则(可以参考《阿里巴巴开发手册》)
- 标识符由英文字符大小写(a - z, A - Z)、数字(0 - 9)、下划线(
_
)和美元符号($
)组成; - 不能以数字开头,不能是关键字;
- 严格区分大小写;
- 包名:多个单词组成是所有单词均小写;
- 类名和接口:大写驼峰命名法;
- 变量名和函数名:多个单词组成时,第一个单词全小写,其他单词采用大写驼峰命名法;
- 常量名:字母全部大写,单词之间用下划线(
_
)分割;
- 标识符由英文字符大小写(a - z, A - Z)、数字(0 - 9)、下划线(
说一下 Java 中的访问控制
Java 主要提供了 3 中访问修饰符 public
、protected
、private
,但实际使用过程中,一共可以形成 4 种访问权限,分别是 public
、protected
、private
、default
,其中 default
是不加任何修饰符时的访问权限。关于 4 种访问权限的对比如下表,✅ 表示可以访问,❌ 表示不可访问。
作用域 | 当前类 | 同一 package 的类 | 子类 | 其他 package 的类 |
---|---|---|---|---|
public | ✅ | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ✅ | ❌ |
default | ✅ | ✅ | ❌ | ❌ |
private | ✅ | ❌ | ❌ | ❌ |
static、final、this、super
- static
static
主要有如下 4 中使用场景:
- 修饰成员变量和成员方法:被
static
修饰的成员属于类,属于静态成员变量,存储在 Java 内存中的 方法区,不属于单个对象,被所有对象共享,而且最好通过类名.静态成员名/静态方法名()
调用; - 静态代码块:定义在类中方法外,先于非静态代码块之前执行(静态代码块 -> 非静态代码块 -> 构造方法) ,而且不管执行多少次创建新对象的操作,静态代码只执行一次;
- 静态内部类:
static
要修饰类时,只有修饰内部类这一种用法。非静态内部类在编译后会隐含保存一个引用,用于指向创建它的外部类,但是静态内部类不存在。即内部类的创建不用依赖外围类的创建,同时内部类也只能使用任意外部类的static
成员变量和方法; - 静态导包:用于导入静态资源,
import static
用于指定导入某一类中的静态资源,然后我们就可以直接使用类中的静态成员变量和方法; - 注意:
abstract
方法不能同时是static
的,因为abstract
方法需要被重写,但static
方法不可以;- 不能从
static
方法内部发出对非静态方法的调用,因为静态方法只能访问静态成员,而非静态方法的调用需要先创建对象; static
不能用于修饰局部变量;- 内部类与静态内部类的区别:静态内部类相对外部类是独立存在的,在静态内部类中无法直接访问外部类中变量和方法。如果要进行访问,则必须
new
一个外部类对象,使用该对象来进行访问,但对于静态变量和静态方法,能够直接调用。而普通的内部类作为外部类的一个成员而存在,能够直接访问外部类属性,调用外部类方法。
- final
- 修饰类时,被修饰的类不能被继承,而且类中所有成员方法均被隐式指定为
final
方法; - 修饰方法时,表明该方法无法被重写;
- 修饰变量时,说明该变量是一个常量。若变量为基本数据类型,则一旦初始化后不能再改变;若变量是引用类型,则初始化后不能指向其他对象。
- this
用于引用类的当前实例,比如我们最常用的构造方法中,注意不能用在 static
方法中;
public class User{int age;public User(int age){this.age = age;}
}
其中 this.age
说明访问的是 User
类中的成员变量,而后面的 age
则代表传入的形参;
- super
用于从子类访问父类中的变量和方法,注意不能用在 static
方法中。
public class Father{String name;public Father(String name){this.name = name;}public Father(){}
}
public class Son extends Father{public Son(String name){super();this.name = name + ".jr";}
}
continue、break 和 return
关键字 | 说明 |
---|---|
continue | 用于循环结构,指跳出当前循环,进入下一次循环 |
break | 用于循环结构,指跳出整个循环体,继续执行循环下面的语句 |
return | 1. return ; 进行同步:直接用 return 结束方法执行,用于没有返回值函数的方法;2. return value; :return 一个特定值,用于有返回值函数的方法 |
while 循环与 do 循环
while
循环结构在循环开始前会判断下一个迭代是否应该继续,可能一次循环体都不执行。
do……while
会在循环的结果来判断是否继续下一轮迭代,至少会执行一次循环体。
final、finally、finalize
- final
final
既是一个修饰符,也是一个关键字,修饰不同对象时,表示的意义也不一样。
- 修饰类: 表示该类无法被继承;
- 修饰变量:若变量是基本数据类型,则其数值一旦初始化后就不能再改变,若变量是引用类型,则在其初始化之后便不能再让其指向另一个对象,但其指向的对象的内容是可变的。
- 修饰方法:表示方法无法被重写,但是允许重载,
private
方法会隐式指定为final
方法。
- finally
finally
是一个关键字,在异常处理时提供finally
块来执行任何清除操作,无论是否有异常被抛出或捕获,finally
块均会被执行,通常用于释放资源。finally
正常情况下一定会被执行,但是在如下两种情况下不会执行:- 对应的
try
未执行,则该try
块的finally
块并不会被执行; - 若
try
块中 JVM 关机,则finally
块也不会执行;
- 对应的
finally
中如果有return
语句,则会覆盖try
或catch
中的return
语句,导致两者无法return
,所以建议finally
中不要存在return
关键字;
- finallize
finallize()
是 Object
类的 protected
方法,子类能够覆盖该方法以实现资源清理工作;
GC
在回收前均会调用该方法,但是 finalize()
方法存在如下问题:
- Java 语言规范不保证
finalize()
方法会被及时执行,也不保证他们一定被执行; finalize()
方法会带来性能问题,因为 JVM 通常在单独的低优先线程中完成finalize
的执行;finalize()
方法中,可将待回收对象赋值给GC Roots
可达的对象引用,从而达到对象再生的目的;finalize()
方法最多由 GC 执行一次(但是可以手动调用对象的finalize
方法);
运算符
算术运算
操作符 | 描述 | 例子 |
---|---|---|
+ | 加法 - 相加运算符两侧的值 | A + B 等于 30 |
- | 减法 - 左操作数减去右操作数 | A – B 等于 -10 |
* | 乘法 - 相乘操作符两侧的值 | A * B等于200 |
/ | 除法 - 左操作数除以右操作数 | B / A等于2 |
% | 取余 - 左操作数除以右操作数的余数 | B%A等于0 |
++ | 自增: 操作数的值增加1 | B++ 或 ++B 等于 21 |
-- | 自减: 操作数的值减少1 | B-- 或 --B 等于 19 |
注意:++
和 --
可以放在操作数之前,也可以放在操作数之后;位于操作数之前时,先自增/减,再赋值;位于操作数之后,先赋值,再自增/减;总结起来就是 符号在前就先加/减,符号在后就后加/减。
关系运算符
运算符 | 描述 | 例子 |
---|---|---|
== | 检查如果两个操作数的值是否相等,如果相等则条件为真。 | (A == B)为假。 |
!= | 检查如果两个操作数的值是否相等,如果值不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是那么条件为真。 | (A> B)为假。 |
< | 检查左操作数的值是否小于右操作数的值,如果是那么条件为真。 | (A <B)为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是那么条件为真。 | (A> = B)为假。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是那么条件为真。 | (A <= B)为真。 |
位运算符
操作符 | 描述 | 例子 |
---|---|---|
& | 如果相对应位都是 1,则结果为 1,否则为 0 | ( A & B ) (A\text{&}B) (A&B),得到 12,即 0000 1100 |
` | ` | 如果相对应位都是 0,则结果为 0,否则为 1 |
^ | 如果相对应位值相同,则结果为 0,否则为1 | ( A B ) (A ^ B) (AB)得到 49,即 0011 0001 |
〜 | 按位取反运算符翻转操作数的每一位,即 0 变成 1,1 变成 0。 | ( A ) ( \text{~} A) ( A) 得到 -61,即1100 0011 |
<< | 按位左移运算符。左操作数按位左移右操作数指定的位数。 | A < < 2 A << 2 A<<2 得到 240,即 1111 0000 |
>> | 按位右移运算符。左操作数按位右移右操作数指定的位数。 | A > > 2 A >> 2 A>>2 得到 15 即 1111 1111 1111 |
>>> | 按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充。 | A > > > 2 A >>> 2 A>>>2 得到 15 即 00001111 0000 1111 00001111 |
逻辑运算符
操作符 | 描述 | 例子 |
---|---|---|
&& | 称为逻辑与运算符。当且仅当两个操作数都为真,条件才为真。 | (A && B) |
` | ` | 称为逻辑或操作符。如果任何两个操作数任何一个为真,条件为真。 |
! | 称为逻辑非运算符。用来反转操作数的逻辑状态。如果条件为true,则逻辑非运算符将得到false。 | !(A && B) |
为真。 |
赋值运算符
操作符 | 描述 | 例子 |
---|---|---|
= | 简单的赋值运算符,将右操作数的值赋给左侧操作数 | C = A + B将把A + B得到的值赋给C |
+= | 加和赋值操作符,它把左操作数和右操作数相加赋值给左操作数 | C + = A等价于C = C + A |
-= | 减和赋值操作符,它把左操作数和右操作数相减赋值给左操作数 | C - = A等价于C = C - A |
*= | 乘和赋值操作符,它把左操作数和右操作数相乘赋值给左操作数 | C _ = A等价于C = C _ A |
/= | 除和赋值操作符,它把左操作数和右操作数相除赋值给左操作数 | C / = A,C 与 A 同类型时等价于 C = C / A |
%= | 取模和赋值操作符,它把左操作数和右操作数取模后赋值给左操作数 | C%= A等价于C = C%A |
<< = | 左移位赋值运算符 | C << = 2等价于C = C << 2 |
>>= | 右移位赋值运算符 | C >> = 2等价于C = C >> 2 |
&= | 按位与赋值运算符 | C&= 2等价于C = C&2 |
^= | 按位异或赋值操作符 | C ^ = 2等价于C = C ^ 2 |
|= | 按位或赋值操作符 | C | = 2等价于C = C | 2 |
条件运算符(? :)
也叫作三元运算符,共有 3 个操作数,且需要判断布尔表达式的值;
variable x = (expression) ? value if true : value if false
instanceof
用于操作对象实例,检查该对象是否是一个特定类型(类类型或接口类型);
( Object reference variable ) instanceof (class/interface type)
equals() 和 ==
==
基本数据类型用 ==
比较的是值,用于引用数据类型时判断两个对象的内存地址是否相等,即两对象是否是同一个对象;
本质来讲,由于 Java 中只有值传递,所以不管是基本数据类型还是引用数据类型,比较的其实都是值,只不过引用类型变量存的值是对象的地址;
equals()
作用也是判断两个对象是否相等,但是 不能用于基本数据类型变量的比较。存在于 Object()
类中,所以所有类都具有 equals()
方法存在两种使用情况:
- 类未覆盖
**equals()**
方法:此时通过equals()
比较该类的两个对象时,等价于==
比较这两个对象,默认使用Object
类中的equals()
方法; - 类覆盖了
**equals()**
方法:一旦覆盖了该方法,则用来比较两个对象的内容是否相等,如我们常用的String、BitSet、Data、File
就覆盖了equals()
方法;
方法
方法的类型
- 无参无返回值;
- 无参有返回值;
- 有参无返回值;
- 有参有返回值;
重载和重写
- 重载(Overload)
重载就是同样方法能够根据输入的不同,做出不同的处理。重载发生在 编译期,而且在同一个类中,方法名必须相同,参数类型、参数个数、参数顺序不同,返回值和访问修饰符可以不同。 总的而言:重载就是同一类中多个同名方法根据不同传参来执行不同的逻辑处理。
- 重写(Override)
重写是当子类继承自父类的相同方法,输入数据一样,但最终响应不同于父类。重写发生在 运行期,是子类对父类允许访问的方法的实现逻辑进行改写。重写方法的方法名、参数列表以及返回值必须相同,抛出的异常范围不超出父类,访问修饰符的范围也不能小于父类。此外,若父类方法别 private/final/static
修饰,则子类无法重写父类方法,但 static
修饰的方法能被再次声明。构造方法是个特例,不能被重写。总结起来就是:重写即子类对父类方法的改造,外部样子不能改变,但能够改变内部逻辑。
- 重载 vs 重写
不同点 | 重载 | 重写 |
---|---|---|
参数列表 | 必须不同 | 必须相同 |
返回类型 | 可不同 | 必须相同 |
访问修饰符 | 可不同 | 不能比父类更严格 |
发生范围 | 同一类中 | 父子类 |
异常范围 | 可修改 | 可以减少或删除,不能抛新异常或范围更广的异常 |
发生阶段 | 编译期 | 运行期 |
深/浅拷贝
- 浅拷贝
浅拷贝是 按位拷贝对象,会创建一个新对象,该对象具有原始对象属性值的精确拷贝。 若属性是基本类型,则拷贝的是基本类型的值;若属性是引用类型(内存地址),则拷贝的是内存地址。因此,一旦其中任一对象改变了该引用类型属性,均会影响到对方;
- 深拷贝
深拷贝会 拷贝所有属性,同时拷贝属性指向的动态分配的内存。当对象和它引用的对象一起拷贝是即发生深拷贝,相比于浅拷贝,深拷贝速度较慢同时花销更大。
- 总结
浅拷贝后,改变其中任一份值都会引起另一份值的改变;而深拷贝后,改变其中任何一份值,均不会对另一份值造成影响;
值传递
推荐阅读:https://juejin.im/post/5bce68226fb9a05ce46a0476
形参和实参
- 形参:方法被调用时需要传递进来的参数,如
func(String name)
中的name
就是一个形参,只有在func
被调用时name
才被分配内存空间,当方法执行完后,name
将自动销毁释放空间; - 实参:方法调用时传入的实际值,在方法调用前就已经被初始化且在方法调用时被传入;
public static void func(String name){System.out.println(name);
}public static void main(String[] args) {//实参String name = "村";func(name);
}
值传递和引用传递
- 值传递
方法被调用时,实参通过形参将其内容副本传入方法内部,此时形参接收的内容实际上是实参的一个拷贝,因此在方法内对形参的任何操作均只针对于实参的拷贝,不会影响到实参原始值的内容。即 值传递的是实参的一个副本,对副本的操作不会影响实参原始值,也即无论形参如何变化,都不会影响到实参的内容。
public static void valueCrossTest(int age,float weight){System.out.println("传入的age:"+age);System.out.println("传入的weight:"+weight);age=33;weight=89.5f;System.out.println("方法内重新赋值后的age:"+age);System.out.println("方法内重新赋值后的weight:"+weight);
}public static void main(String[] args) {int a=25;float w=77.5f;valueCrossTest(a,w);// a = 25,原始值不收影响System.out.println("方法执行后的age:"+a);// w = 77.5,原始值不收影响System.out.println("方法执行后的weight:"+w)
}
- 引用传递
引用即指向真实内容的地址值,在方法调用时,实参的地址被传递给相应形参,在方法体内,形参和实参指向同一个地址内存,因此此时操作形参也会影响到实参的真实内容。
但 Java 中并 不存在引用传递,因为 无论是基本类型还是引用类型,在实参传入形参时,均为值传递,即传递的都是一个副本,而非实参内容本身。
- 总结
如果是对基本数据类型的数据进行操作,由于实参原始内容和副本都是存储实际值,并且处于不同栈区,因此对形参的操作,实参原始内容不受影响。
如果是对引用类型的数据进行操作,分两种情况,
- 一种是形参和实参保持指向同一个对象地址,则形参的操作,会影响实参指向的对象的内容。
public static void PersonCrossTest(Person person){System.out.println("传入的person的name:" + person.getName());person.setName("我是张小龙");System.out.println("方法内重新赋值后的name:" + person.getName());
}
- 另一种是形参被改动指向新的对象地址(如重新赋值引用),则形参的操作,不会影响实参指向的对象的内容。
public static void PersonCrossTest(Person person){System.out.println("传入的person的name:" + person.getName());person=new Person();person.setName("我是张小龙");System.out.println("方法内重新赋值后的name:" + person.getName());
}
面向对象
面向对象 vs 面向过程
推荐阅读:https://www.zhihu.com/question/27468564/answer/757537214
- 面向对象(Object Oriented)
面向过程是一种 对现实世界理解和抽象的方法,更容易维护、复用、扩展。最主要的特点就是 继承、封装、多态,所以 设计出的系统耦合性较低,但比起面向过程性能要低。
- 面向过程(Procedure Oriented)
面向过程是一种 以过程为中心 的编程思想,以正在发生为主要目标进行编程,不同于面向的的是谁受影响。最主要的不同就在于 封装、继承、多态,其性能比面向对象更高。
- 总结
面向对象的方式使得每个类都各司其职,最后整合到一起来共同完成一个项目,而面向过程则是让一个类中的功能越来越多,就像一个全栈工程师能够一个人搞定所有事。
封装、继承、多态
- 封装
将客观事物封装为抽象的类,同时类能把自己的数据和方法只让可信的类或对象进行操作,对不可信的类进行信息隐藏。即把属于同一类事物的共性(属性与方法)归到一个类,从而方便使用。
通过 封装,实现了 专业分工,将能实现特定功能的代码封装为独立实体,供我们在需要时调用。此外,封装还 隐藏了信息以及实现细节,使得我们通过访问权限权限符就能将想要隐藏的信息隐藏起来。
- 继承
可以使用现有类的所有功能,且无需重写现有类来进行功能扩展,即个性对共性的属性与方法的接受,并加入特性所特有的属性与方法。通过继承的新类叫做 子类/派生类,被继承的类叫做 父类/基类/超类,具有如下特点:
- 子类拥有父类对象所有属性和方法,但父类中的私有属性和方法,子类是无法访问的;
- 子类可以对父类进行扩展;
- 子类可以用自己的方式来实现父类的方法;
- 多态
多态是允许 将父对象设置为和一个或多个其子对象相等的技术,赋值后,父对象能够根据指向的子类对象的特性以不同方式运作,即 父类引用指向子类对象实例,有 重载和重写 两种实现方式。具有如下特点:
- 对象类型不可变,但引用类型可变;
- 对象类型和引用类型之间有继承(类)/实现(接口)的关系;
- 方法具有多态性,但属性不具有;
- 若子类重写了父类方法,则真正执行的是子类覆盖的方法,若子类未覆盖父类方法,则调用父类的方法。
成员变量 vs 局部变量 vs 静态变量
不同 | 语法 | 存储位置 | 生命周期 | 初始化值 | 调用方式 | 别名 |
---|---|---|---|---|---|---|
成员变量 | 1、 属于类 | |||||
2、能被访问控制符、static、final | ||||||
等修饰 | 堆 | 与对象共存亡 | 有,基本数据类型为对应默认值,而对象统一为 null | 对象调用 | 实例变量 | |
局部变量 | 1、属于方法(方法中的变量或参数) | |||||
2、不能被访问控制符及 static | ||||||
修饰,但可以被 final | ||||||
修饰 | 栈 | 与方法共存亡 | 无,必须定义赋值后使用 | |||
静态变量 | 1、属于类 | |||||
2、被 static | ||||||
修饰,被所有类对象共用 | 方法区 | 与类共存亡 | 同成员变量初始化值 | 类名调用(推荐)、对象调用 | 类变量 |
构造方法的特点
- 方法名与类名同名;
- 无返回值,但不能用
void
关键字声明; - 生成类对象时自动执行,无需显式调用;
抽象类 & 接口
- 接口
- 接口中所有方法默认是
public
,而且不能有实现(Java 8 之前,Java 8 开始可以有默认实现); - 接口中所有变量均为
static、final
,不能有其他变量; - 一个类可以实现多个接口(通过
implements
关键字),而且接口自身可以通过extends
来扩展多个接口; - 接口是对行为的抽象,属于行为规范;
- 抽象类
- 抽象类中既可以有抽象方法,也可以有非抽象的方法;
- 一个类只能实现一个抽象类;
- 抽象方法可以被
public、protected、default
修饰,但不能用private
,否则不能被重写; - 抽象是对类的抽象,是一种模板设计;
Object 类中常见方法
方法 | 说明 |
---|---|
public final native Class<?> getClass() | 用于返回当前运行时对象的 Class 对象,使用了final 关键字修饰,故不允许子类重写 |
public native int hashCode() | 用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的 HashMap |
public boolean equals(Object obj) | 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写用户比较字符串的值是否相等 |
protected native Object clone() throws CloneNotSupportedException | 用于创建并返回当前对象的一份浅拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为 true。Object 本身没有实现 Cloneable 接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException 异常 |
public String toString() | 返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法 |
public final native void notify() | 不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个 |
public final native void notifyAll() | 不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程 |
public final native void wait(long timeout) throws InterruptedException | 不能重写。暂停线程的执行注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间,调用该方法后当前线程进入睡眠状态,知道如下时间发生: |
- 其他线程调用该对象的
notify()/notifyAll()
方法; - 时间间隔到了;
- 其他线程调用了
interrupt()
中断该线程; |
|public final void wait(long timeout, int nanos) throws InterruptedException
| 多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒 |
|public final void wait() throws InterruptedException
| 跟之前的 2 个 wait 方法一样,只不过该方法一直等待,没有超时时间这个概念 |
|protected void finalize() throws Throwable { }
| 实例被垃圾回收器回收的时候触发的操作 |
hashCode & equals
推荐阅读:https://juejin.im/post/5a4379d4f265da432003874c
equals
- 重写
equals()
方法的准则:
准则 | 说明 |
---|---|
自反性 | 对任意非空引用值 x |
,x.equals(x) | |
应该返回 true | |
对称性 | 对于任何非空引用值 x |
和 y | |
,当 y.equals(x) | |
返回 true | |
时,x.equals(y) | |
也应返回 true | |
传递性 | 对于任何非空引用值x、y |
和 z | |
,如果 x.equals(y) | |
返回 true | |
, 并且 y.equals(z) | |
返回 true | |
,那么 x.equals(z) | |
也应返回 true | |
一致性 | 对于任何非空引用值 x |
和 y | |
,多次调用 x.equals(y) | |
始终返回 true | |
或始终返回 false | |
, 前提是对象上 equals | |
比较中所用的信息没有被修改 | |
非空性 | 对于任何非空引用值 x |
,x.equals(null) | |
都应返回 false |
hashCode
hashCode
用于返回对象 hash
值,主要是为了加快查找的快捷性,因为 hashCode()
是 Object
类中的方法,所以所有 Java 类均有 hashCode()
,在 HashTable
和 HashMap
这类的散列结构中,均是通过 hashCode()
来查找在散列表中位置,通过 hashCode
能够较快的茶道小内存块。
为什么重写 equals()
必须重写 hashCode()
- 若两个对象相等,则
hashCode()
一定也相同,因为equals()
是绝对可靠的; - 两个对象相等,则两个对象分别调用
equals()
方法也返回true
; - 两个对象有相同的
hashCode()
,他们不一定相等,因为hashCode()
不是绝对可靠的; - 如果重写了
equals()
,但保留hashCode()
的实现不变,则可能出现两者相等,但hashCode
却不一样; - 因此,一旦重写了
equals()
方法,则必须重写hashCode()
,hashCode()
的默认行为是对堆上的对象产生独特值。如果没有重写hashCode()
,则该class
的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
序列化与反序列化
定义
- 序列化:指将对象转换为字节序列的过程;
- 反序列化:指将字节序列转换为目标对象的过程;
需要序列化的场景
当 Java 对象需要在网络上传输或者持久化存储到文件中时,我们就需要对象进行序列化;
如何实现序列化
要实现序列化,只需要让类实现 Serializable
接口即可,此时就标注该类对象能够被序列化;
针对类中某些数据不想序列化时,可以使用 transient
关键字来实现,例如:
// 通过关键字 transient 修饰,表明不参与序列化
transient private String telephone;
Java 进阶面试知识点
异常
异常类层次结构
从结构图可以看出,所有异常均继承自 Throwable
类,它有两个重要的子类:Exception
和 Error
,各自又包含大量子类。
- Exception
程序本身可以处理的异常,又可以分为 受检异常 和 非受检异常 ,受检异常 可以用 try...catch...
语句进行捕获处理,而且能从异常中恢复。但 非受检异常 是程序运行时错误,会导致程序崩溃而无法恢复。
- Error
程序无法处理的错误,表示程序运行过程中较严重的问题,大多与 coder 所做操作无关,而是代码运行时 JVM 出现的问题。此时说明故障发生于虚拟机本身、或者发生在虚拟机试图执行应用时。
Throwable 常用方法
方法 | 说明 |
---|---|
public String getMessage() | 返回异常发生时的简要描述 |
public String toString() | 返回异常发生时的详细信息 |
public String getLocalizeMessage() | 返回异常对象的本地化信息,若子类重写该方法,可以生成本地化信息,若未重写,则返回信息同 getMessage() |
方法 | |
public void printStackTrace() | 在控制台中打印异常对象封装的异常信息 |
try-catch-finally 和 try-with-resources
- try-catch-finally
- try :用于捕获异常,后接零个或多个
catch
,没有catch
则必须加上finally
; - catch:用于处理
try
捕获到的异常; - finally:无论是否捕获/处理异常,
finally
块中内容均会执行,就算try
或catch
中有return
语句,finally
中代码也将在方法返回之前执行;
- try :用于捕获异常,后接零个或多个
- try-with-resources
当我们有必须要关闭的资源时,建议优先使用 try-with-resources
,这样写出的代码更加简短清晰。
- 两者对比
// try-catch-finally
Scanner scanner = null;
try {scanner = new Scanner(new File("D:/demo.txt"));while (scanner.hasNext()) {System.out.println(scanner.nextLine());}
} catch (FileNotFoundException e) {e.printStackTrace();
} finally {if (scanner != null) {scanner.close();}
}
// try-with-resources
try (Scanner scanner = new Scanner(new File("D:/demo.txt"))) {while (scanner.hasNext()) {System.out.println(scanner.nextLine());}
} catch (FileNotFoundException e) {e.printStackTrace();
}
IO
IO 流的定义
IO 流是一种数据的流从源头流到目的地,如文件拷贝操作,输入和输出流都包括了。输入流从文件中读取数据存储到进程,然后输出流从进程中读取数据写入到目标文件;
IO 分类
Java IO 共涉及 40 多个类,均从如下 4 个抽象类中派生而来:
InputStream
:所有输入流的基类,字节输入流OutputStream
:所有输出流的基类,字节输出流Reader
:所有输入流的基类,字符输入流Writer
:所有输出流的基类,字符输出流
字节流 vs 字符流
推荐阅读:https://www.zhihu.com/question/39262026
- 字节流
1 Byte = 8 bit
,字节流处理的最基本单位为单个字节,默认不使用缓冲区,而是直接操作磁盘文件,常用于处理音频、图片等媒体文件二进制数据;
- 字符流
1 char = 2 Byte = 16 bit
,字符流处理的最基本的单元是 Unicode 码元,更适合对于操作需要通过 IO 在内存中频繁处理字符串的情况,因为字符流具有缓冲区,性能更高,常用于处理文本数据;
- 有了字节流,为什么还要字符流?
字节流由 JVM 将字节转换而来,但是该过程非常耗时,而且一旦编码未知就很容易导致乱码。为了解决这个问题,所以提供了一个直接操作字符的接口,从而方便我们对字符进行流处理。
BIO、NIO 和 AIO
推荐阅读:https://zhuanlan.zhihu.com/p/83597838
- BIO (Blocking I/O)
BIO 特点:就是 IO 执行的两个阶段用户进程都会阻塞住;
最传统的一种 IO 模型,在读写过程中会发生阻塞现象。当线程发出 IO 请求后,内核查看数据是否就绪,若未就绪就等待数据就绪,此时用户线程处于阻塞状态,用户线程交出 CPU。一旦数据准备就绪,内核就将数据拷贝到用户线程并返回结果给用户线程,此时用户线程才接触阻塞状态。
同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。适合于连接数较小(小于单机 1000)且固定的框架,该方式对服务器资源要求较高,并发局限于应用中,是 JDK 1.4 之前唯一的选择。该模式让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。
线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的,因此,需要更加高效的模式来处理更高的并发量。
假设一个烧开水的场景,有一排水壶在烧开水,BIO 的工作模式就是, 一个线程对应一个水壶,在当前水壶烧开后,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
总结而言,一个典型的读操作流程大致为:
- 用户进程调用
recvfrom
系统调用,内核此时开始 IO 第一阶段:准备数据,将数据拷贝到内核缓冲区中; - 等到数据拷贝到操作系统内核缓冲区后,进入 IO 第二阶段:将数据从内核缓冲区拷贝到用户内存,然后内核返回结果,用户进程才会解除
**block**
状态,重新运行起来;
- NIO (Non-blocking/New I/O)
NIO 特点:用户进程需要不断的主动询问内核数据准备好没有;
用户线程不断询问内核数据是否就绪,即 NIO 不会交出 CPU,而是一直占用 CPU,直到数据准备好。
NIO 是一种 同步非阻塞 的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio
包,提供了 Channel , Selector,Buffer 等抽象,适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器。NIO 中的 N 可以理解为 Non-blocking
,不单纯是 New
。它支持 面向缓冲 的,基于通道 的 I/O 操作方法。
NIO 提供了与传统 BIO 模型中的 Socket
和 ServerSocket
相对应的 SocketChannel
和 ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于 高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
还拿烧开水场景来说,NIO 的做法是让一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
- AIO (Asynchronous I/O)
AIO 中,有一个线程不断去轮询多个 Socket 的状态,只有当 Socket 真正有读写事件时,才真正调用实际的 IO 读写操作。
AIO 也就是 NIO 2,在 Java 7 中引入了 NIO 的改进版 NIO 2,它是 异步非阻塞 的 IO 模型,适合连接数目多且连接比较长(重操作)的架构,比如相册服务器。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。
AIO 比 NIO 效率高的原因在于在 NIO 中,不断询问 Socket 状态是通过用户线程去实现的,但在 AIO 中,轮询 Socket 状态是内核来进行的,效率比用户线程高。
对应烧开水场景中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。
读取大数据量文件,如何选择流?
选用字节流时,选择 BufferedInputStream
和 BufferedOutputStream
;
选用字符流时,选用 BufferedReader
和 BufferedWriter
;
NIO
NIO 与 IO 的区别?
IO | NIO |
---|---|
面向流 | 面向缓冲 |
阻塞 IO | 非阻塞 IO |
无 | 选择器 |
少量连接,但每次要发送大量数据 | 大量连接,但连接每次只发送少量数据 |
NIO 是为了弥补传统 IO 不足而诞生,但也存在如下缺点:因为 NIO 是面向缓冲区的操作,每次的数据处理均是对缓冲区进行的,所以在数据处理之前必须判断缓冲区的数据是否完整或已经读取完毕,如果没有,则假设数据只读取了一部分,对不完整的数据处理无任何意义。
NIO 核心组件
- NIO 核心组件可以分为如下三个:
- channel
- buffer
- selector
- channel
一个 channel
(通道)代表和某一实体的连接,该实体可以是文件、网络套接字等。即 NIO 中的 channel
相当于一个中介,用于程序和操作系统底层 I/O 服务进行交互;
一般最常用的通道实现有:
FileChannel
:读写文件;DatagramChannel
:UDP 协议网络通信;SocketChannel
:TCP 协议网络通信;ServerSocketChannel
:监听 TCP 连接;
- buffer
NIO 中的缓冲区不是一个简单的 byte
数组,而是封装过的 Buffer
类,NIO 提供了 ByteBuffer、CharBuffer、IntBuffer
等,他们之间的区别在于读写缓冲区时的单位长度不一样;
buffer
有如下基本操作来进行读写数据:
- 将数据写入
buffer
; - 调用
buffer.flip()
; - 将数据从
buffer
中读取数据; - 调用
buffer.clean
或buffer.compact()
;
- selector
选择器是一个特殊组件,用于采集各个通道的状态。现将通道注册到选择器,并设置好关心的时间,然后就可以通过调用 select()
方法,等待事件发生即可;
集合
Collections 工具类
java.util.Collections
工具类提供了许多常用方法,而且都是静态(static
)的,可以分为如下几类:
- 排序(主要针对 List 接口)
方法 | 说明 |
---|---|
void reverse(List list) | 反转指定 List |
集合中元素的顺序 | |
void shuffle(List list) | 对 List |
中的元素进行随机排序(洗牌) | |
void sort(List list) | 对 List |
里的元素根据自然升序排序 | |
void sort(List list, Comparator c) | 自定义比较器进行排序 |
void swap(List list, int i, int j) | 将指定 List |
集合中 i | |
处元素和 j | |
处元素进行交换 | |
void rotate(List list, int distance) | 将所有元素向右移位指定长度,如果 distance |
等于 size | |
那么结果不变 |
- 查找、替换(主要针对 Collection 接口)
方法 | 说明 |
---|---|
int binarySearch(List list, Object key) | 使用二分搜索法,以获得指定对象在 List |
中的索引,前提是集合有序 | |
int max(Collection coll) | 返回最大元素 |
int max(Collection coll, Comparator c) | 根据自定义比较器,返回最大元素 |
int min(Collection coll) | 返回最小元素 |
int min(Collection coll, Comparator c) | 根据自定义比较器,返回最小元素 |
void fill(List list, Object obj) | 使用指定对象填充 List |
int frequency(Collection c, Object o) | 返回指定集合中指定对象出现的次数 |
int indexOfSubList(List list, List target) | 统计 target |
在 list | |
中第一次出现的索引,找不到则返回 -1 | |
boolean replaceAll(List list, Object oldVal, Object newVal) | 替换 |
- 同步控制
方法 | 说明 |
---|---|
synchronizedCollection(Collection<T> c) | 返回指定 collection |
支持的同步(线程安全的)collection | |
synchronizedList(List<T> list) | 返回指定列表支持的同步(线程安全的)List |
synchronizedMap(Map<K,V> m) | 返回由指定映射支持的同步(线程安全的)Map |
synchronizedSet(Set<T> s) | 返回指定 set |
支持的同步(线程安全的)set |
- 设置不可变集合
方法 | 说明 |
---|---|
emptyXxx() | 返回一个空的不可变的集合对象,此处的集合可以是 List |
、Set | |
、 Map | |
singletonXxx() | 返回一个只包含指定对象(只有一个或一个元素)的不可变集合对象,此处的集合可以是 List |
、Set | |
、 Map | |
unmodifiableXxx() | 返回指定集合对象的不可变视图,此处的集合既可以是List |
、Set | |
、 Map |
Arrays 工具类
方法 | 说明 |
---|---|
sort() | 排序 |
binarySearch | 二分查找 |
equals() | 比较 |
fill() | 填充 |
asList() | 转换为列表 |
toString() | 转换为字符串 |
copyOf() | 复制 |
sort()
// 数字排序
int[] a = {9, 8, 7, 2, 3, 4, 1, 0, 6, 5};
Arrays.sort(a);
for (int i = 0; i < a.length; i++) {System.out.print(a[i] + "\t");
}
System.out.println();// 字符串排序
String[] a1 = {"a", "A", "b", "B"};
Arrays.sort(a1);
for (int i = 0; i < a1.length; i++) {System.out.print(a1[i] + "\t");
}
System.out.println();// 字符串反向排序
String[] a2 = {"c", "C", "d", "D"};
Arrays.sort(a2, Collections.reverseOrder());
for (int i = 0; i < a2.length; i++) {System.out.print(a2[i] + "\t");
}
System.out.println();// 数字反向排序,此时要使用封装类
Integer[] a4 = {9, 8, 7, 2, 3, 4, 1, 0, 6, 5};
Arrays.sort(a4, Collections.reverseOrder());
for (int i = 0; i < a.length; i++) {System.out.print(a4[i] + "\t");
}
System.out.println();// 区间排序
int[] a5 = {9, 8, 7, 2, 3, 4, 1, 0, 6, 5};
Arrays.sort(a5, 3, 7);
for (int i = 0; i < a5.length; i++) {System.out.print(a5[i] + " ");
}
System.out.println();
bianrySearch()
int[] b = new int[]{4, 43, 12, 312, 87, 21};
System.out.println("原数组为:");
for (int dim1 : b) {System.out.print("" + dim1 + " ");
}
// 排序
Arrays.sort(b);
System.out.println("\n排序后为:");
for (int x : b) {System.out.print(x + " ");
}
System.out.println();
int index = Arrays.binarySearch(b, 312);
System.out.println("关键字2的返回值为:" + index);
equals()
String[] str1={"hello","world"};
String[] str2={"hello","world"};
System.out.println("Arrays.equals(e, f):" + Arrays.equals(str1, str2));
fill()
Integer[] a = new Integer[10];
// 全部填充
Arrays.fill(a, 12);
System.out.println("当前数组容器:"+Arrays.toString(a));// 区间填充
// 填充的开始位
Integer startIndex = 1;
// 填充的结束位
Integer endIndex = 3;
Arrays.fill(a, startIndex, endIndex, 8);
System.out.println("当前数组容器:"+Arrays.toString(a));
asList()
Integer[] array1 = {5, 6, 9, 3, 2, 4};
List list = Arrays.asList(array1);
for (int i = 0; i < list.size(); i++) {System.out.print("\t" + list.get(i));
}
toString()
String[] array02 = {"悟空", "八戒", "唐僧"};
System.out.println(array02);
System.out.println(Arrays.toString(array02));
copyOf()
// array 和 array02 互不相干int[] array = {5, 6, 9, 3, 2, 4};
System.out.println("array1的数组长度:" + array.length);
// 根据实际情况扩容
int[] array2 = Arrays.copyOf(array, array.length + 1);
System.out.println("array2的数组长度:" + array2.length);array2[0] = 100;
System.out.println("array1:" + Arrays.toString(array));
System.out.println("array2:" + Arrays.toString(array2));
有关集合的更多内容,可以参看 Java 容器知识点总结。
泛型
推荐阅读:https://juejin.im/post/5b614848e51d45355d51f792
什么是泛型?使用泛型的好处?
泛型是 JDK 5 后引入的新特性,提供了 编译期的类型安全检测机制,确保将正确类型的对象放入集合,避免了运行时报 ClassCastException
。其本质是 参数化类型,即所操作的数据类型被指定为一个参数。
注意:虽然编译期在编译过程中移除了参数的类型信息,但会保证类或方法内部参数类型的一致性;
泛型的工作机制?
类型擦除:指 Java 编译器生成的字节码不包含泛型信息;
泛型是通过 类型擦除 来实现,编译期在 编译时擦除所有类型相关信息,因此在运行时不存在任何类型相关的信息。我们无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。
泛型的三种使用方式
- 泛型类:通过传入不同类型的数据,可以存储相应类型的数据;
// 一个泛型类声明如下,在实例化时,必须指定 T 的具体类型
public class Generic<T>{ private T key;public Generic(T key) { this.key = key;}public void setKey(T key){this.key = key;}public T getKey(){ return key;}
}
- 泛型接口:泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中;若泛型接口传入类型参数时,实现该泛型接口的实现类,则所有使用泛型的地方都要替换成传入的实参类型;
// 一个泛型接口声明如下,要实现该接口,可以不指定具体类型,也可以指定具体类型
public interface Generator<T> {public T method();
}
// 不指定具体类型
public class GeneratorImpl<T> implements Generator<T>{@Overridepublic T method(){...}
}
// 指定具体类型
public class GeneratorImpl<T> implements Generator<String>{@Overridepublic String method(){...}
}
- 泛型方法:可以存在泛型类中,也可以存在普通类中,如果用泛型方法能够解决的问题,就尽量使用泛型方法。
// 一个具体的泛型方法声明如下,使用该方法时,我们可以传入不同类型的参数,提高复用率
public static < E > void printArray( E[] inputArray )
{ for ( E element : inputArray ){ System.out.printf( "%s ", element );}System.out.println();
}
泛型通配符?
- T(type):表示具体的一个 Java 类型;
- K V(key value):代表 Java 键值对;
- E(element):代表 Element;
- ?:表示不确定的 Java 类型;
限定通配符和非限定通配符?
限定通配符 对类型进行限制,分为两种(<? extends T>
和 <? super T>
).<? extends T>
通过确保类型必须为 T
的子类来设定类型的上界;而 <? super T>
通过确保类型必须是 T
的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则将导致编译错误。
非限定通配符,<?>
,可以用任意类型来替代。
可以把 List 传递给一个接受 List
不可以,因为 List<String>
中只能用来存储 String
,而 List<Object>
中却能存储任意类型的对象(String、Integer
等) 。
Array 中是否可以使用泛型?
不可以,但是用 List
来代替 Array
,因为 List
可以提供编译期的类型安全保证,但 Array
不能。
多线程
线程、进程、程序
推荐阅读:https://juejin.im/post/5c932660f265da612524ad6d
- 程序
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
- 线程
有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元,是被系统独立调度和分派的基本单位。线程是比进程更小的执行单位,一个进程在执行过程中可能会产生多个线程,但 不同于进程是同类的多个线程共享同一块内存空间和一组系统资源。
- 进程
进程是据有独立功能的程序在某个数据集合上的一次运行活动,也是操作系统进行资源分配和保护的基本单位,因此是动态的。
从 原理 角度上看:进程是支持程序执行的一种系统机制,对 CPU 上运行程序的活动规律进行抽象。
从 实现 角度看:进程是一种数据结构,用来准确的刻画运行程序的状态和系统动态变化状况。
系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。
换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
线程基本状态
状态 | 说明 |
---|---|
NEW | 初始状态,创建线程,但还未调用 start() |
方法 | |
RUNNABLE | 运行状态,“就绪” 和 “运行” 两种状态统称 “运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,线程进行等待状态,进入该状态表示当前线程需要等待其他线程做出通知或中断 |
TIME_WAITING | 超时等待状态,不同于 WAITING |
,经过指定时间后可以自行返回 | |
TERMINATED | 终止状态,表示线程执行完毕 |
上图源自《Java 并发编程艺术》,可以看出一个线程的状态变迁过程:
线程创建后处于 NEW
状态,然后调用 start()
方法后开始运行,此时处于 READY
状态,接着一旦可运行状态的线程获得了 CPU 时间片之后,就处于 RUNNING
状态。如果线程执行了 wait()
方法,则进入 WAITING
状态。进入 WAITING
状态的线程池需要依靠其他线程的通知后才能返回 RUNNING
状态。而 TIME_WAITING
状态相当于在 WAITING
状态的基础上增加了超时限制,当超时后线程将自动返回 RUNNABLE
状态。当线程调用同步方法后,如果没有获取到锁,线程将进入 BLOCKED
状态。同时,如果线程执行了 RUNNABLE
的 run()
方法,线程将进入 TERMINATED
状态。
使用多线程的三种方式
- 继承 Thread 类
需要实现 run()
方法,其底层也实现了 Runnable
接口。当调用 start()
方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run()
方法。
public class MyThread extends Thread{@Overridepublic void run(){super.run();System.out.println("MyThread");}
}
public class Main{public static void main(String[] args){MyThread myThread = new MyThread();myThread.start();System.out.println("运行结束");}
}
- 实现 Runnable 接口
使用 Runnable
实例再创建一个 Thread
实例,然后调用 Thread
实例中的 start()
方法来启动线程。
public class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("MyRunnable");}
}
public class Run {public static void main(String[] args) {Runnable runnable=new MyRunnable();Thread thread=new Thread(runnable);thread.start();System.out.println("运行结束!");}}
- 实现 Callable 接口
相比于 Runnable
,Callable
可以有返回值,结果通过 FutureTask
进行封装;
public class MyCallable implements Callable<Integer> {public Integer call() {return 123;}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable myCallable = new MyCallable();FutureTask<Integer> futureTask = new FutureTask<>(myCallable);Thread thread = new Thread(futureTask);thread.start();System.out.println(futureTask.get());System.out.println("运行结束!");}
线程优先级
每个线程都有各自优先级,默认情况下均为 Thread.NORM_PRIORITY(5)
。线程优先级能在程序中表明该线程的重要性,若许多线程均处于就绪状态,系统就会根据优先级来决定哪一个线程优先进入运行状态。线程优先级均在 Thread.MIN_PRIORITY(1)
到 Thread.MAX_PRIORITY(10)
之间,数字越大,优先级越高,线程优先级有以下两个特点:
- 继承性:如 A 线程启动 B 线程,则 B 线程的优先级和 A 线程一样;
- 随机性:线程优先级高的不一定每次都先执行完,只能说是优先开始执行;
反射
推荐阅读:https://zhuanlan.zhihu.com/p/80519709
反射的定义及作用?
反射(Reflection
)允许运行中的 Java 程序获取自身信息,并且可以操作类或对象的内部属性。核心 是 JVM 在运行时才动态加载类或调用方法/访问属性,无需事先知道运行对象是谁,主要提供如下功能:
- 运行时 判断任意一个对象所属类;
- 运行时 构造任意一个类的对象;
- 运行时 判断任意一个类所具有的成员变量和方法;
- 运行时 调用任意一个对象的方法;
- 生成动态代理
Class
和 java.lang.reflect
共同对反射提供了支持,java.lang.reflect
类库中主要包含了如下三个类:
- Field:用
get()
和set()
方法来读取和修改Field
对象相关联的成员属性; - Method:利用
invoke()
方法调用与Method
对象相关联的方法; - Constructor:利用
Construcctor
中的newInstance()
创建新的对象;
反射的优缺点
- 静态和动态编译
- 静态编译:在编译时确定类型,绑定对象
- 动态编译:在运行时确定类型,绑定对象
两者区别在于,动态编译能最大化支持多态,降低类的耦合性。
- 优点
- 运行时判断类,动态加载类,提高了代码的灵活度;
- 最大化支持多态,降低类的耦合性;
- 缺点
- 性能开销:反射相当于一系列解释操作,告知 JVM 要做的事,比直接的代码要慢;
- 安全限制:反射要求程序必须在一个无安全限制的环境下运行,如果一个程序必须要在安全的环境中运行,那么就无法应用反射了;
- 内部暴露:我们虽然可以动态操作改变类的属性,但同时也暴露了类的内部;
反射的基本运用
如何利用反射创建对象
- 通过
Class.forName()
获取一个对象;
# 前提是已知类的全路径名
Class class2 = Class.forName("全限名");
- 使用
.class
属性;
# 前提是已知要操作的类
Class class2 = User.class;
- 使用类对象的
getClass()
方法;
User user = new User();
Class class3 = user.getClass();
反射中的一些注意事项
- 反射会额外消耗一定系统资源,因此如果不需要动态创建一个对象,那么就尽量别用反射;
- 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题;
注解
推荐阅读:https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html
什么是注解?
Java 注解是附加在代码中的一些元信息,从 Java 5 开始引入的特性,用于一些工具在编译、运行时进行解析和使用,起着 **说明、配置的功能。**不会也不能影响代码实际逻辑,只起辅助性作用。
注解的用途?
- 生成文档,比如
@param、@return
等用于给方法生成文档; - 跟踪代码依赖性,实现替代配置文件功能;
- 在编译时进行格式检查,如
@Override
放在方法前,表明该方法要覆盖父类方法;
注解的原理?
注解实质上是继承了 Annotation
的特殊接口,具体实现类是 Java 运行时生成的动态代理类。当我们通过反射机制获取注解的时候,返回的是 Java 运行时所生成的动态代理对象。通过该代理对象来调用自定义注解(方法)的方法,最终会调用 AnnotationInvocationHandler
的 invoke()
方法。方法从 memberValues
中索引出对应值(memberValues
是一个 Map
,来源于 Java 常量池)。
元注解
java.lang.annotation
中提供了四种元注解,用于注解其他注解(一般用于我们自定义注解时)。
元注解 | 说明 |
---|---|
Retention | 定义注解声明周期: |
SOURCE
:编译阶段CLASS
:类加载时RUNTIME
:始终不丢弃 |
|Documented
| 是否将该注解信息加入 Java 文档 |
|Target
| 定义注解作用域CONSTRUCTOR
FIELD
LOCAL_VARIABLE
METHOD
PACKAGE
PARAMETER
TYPE
|
|Inherited
| 定义注解和子类的关系 |
自定义注解的规则
- 注解应该定义为
@interface
; - 参数成员只能用
public
或default
两个访问控制符; - 参数成员只能有 8 大基本数据类型和
String、Enum、Class、annotation
等数据类型及其数组; - 获取类方法或字段的注解信息,必须通过反射技术来获取
Annotation
对象; - 注解中可以定义成员;