在 JAVA 语言中容易被忽略的问题
摘自于Leetcode java突击面试宝典,仅供个人学习参考,转载请注明出处!
1. 基本的数据结构
直接量 概念:
直接量是在程序中直接出现的常量值
整数类型的直接量 默认是
int
类型浮点类型的直接量 默认是
double
类型整数类型的直接量 (默认是 $int$ 类型) 赋值 给整数类型的变量
将整数类型的直接量赋值给整数类型的变量时,只要直接量没有超出变量的取值范围,即可直接赋值;
如果直接量超出了变量的取值范围,则会导致编译错误。
如果整数类型的直接量 超过了$int$ 的取值范围,就必须在直接量后面加上
l
,将变量显性声明为long
类型,否则会导致编译错误如果要将直接量表示成
float
类型,则必须在其后面加上字母 F 或 f。将double
类型的直接量赋值给float
类型的变量是不允许的,会导致编译错误。
2. 面向对象
静态,Java 的类成员(成员变量、方法等)可以是静态的或实例的。使用关键字
static
修饰的类成员是静态的类成员,不使用关键字static
修饰的类成员则是实例的类成员。被
static
修饰的方法,只能访问静态的方法/变量 ,不能访问实例的类成员而 实例方法 既可以访问 实例的类成员,也可以访问静态的类成员
3. 重载的条件
- 函数名必须相同
- 参数列表必须不同(个数不同,类型不同,参数类型的排列顺序不同)
- 返回类型 可以相同 可以不同
- 仅 返回类型不同 不能构成方法的重载
- 重载是发生在编译时,因为编译器可以根据参数的类型类选择使用哪种方法
4. 重写
重载 与 重写 名字相似但 两者是完全不同的关系,
- 方法的重写 是描述子类与父类之间的,而重载指的的在一个类中
- 重写的方法必须要和父类保持一致,包括返回值类型,方法名,参数列表
5. 类中初始化的顺序
- 静态属性 初始化
- 静态方法块初始化
- 普通属性 初始化
- 普通方法块初始化
- 构造函数 初始化
- 普通方法
- 例:
1 | public class MyClass |
6. Static
static 修饰 成员变量 称为 类变量
可以直接通过 类名.类变量名 访问或修改 不需要通过对象访问
static 修饰 成员函数 称为 静态方法
- 可以直接通过 类名.方法名 调用 ,不需要通过对象访问
- 静态方法 只能访问静态的成员变量 或者静态的方法
- 静态方法中 不能含有
this
关键字
static 修饰 内部类(static只能修饰内部类)了解
一般内部类的定义都是如下形式
1
2
3
4
5
6
7
8
9
10
11
12public class Outter
{
public static String name = "Outter";
public class Inner //只有public 修饰符
{
public void test()
{
System.out.println("Inner为测试而生!");
}
}
}这种内部类的实例化方法 是:
1
Outter.Inner inner=new Outter().new Inner();
而被
static
修饰了的内部类 是如下形式1
2
3
4
5
6
7
8
9
10
11
12public class Outter
{
public static String name = "Outter";
public static class StaticInner //内部类被 static修饰
{
public void test()
{
System.out.println("StaticInner为测试而生!");
}
}
}被
static
修饰的实例化方法是:1
Outter.StaticInner staticInner=new Outter.StaticInner();
7. Final
final 修饰 类
表示该类不可被继承,并且该类的成员方法 也会被隐式的指定为final,成员变量 根据需要 自己设置是否被final 修饰
final 修饰 方法
表示这个方法不能被子类重写
final 修饰 变量
- 修饰基本数据类型 表示该值不可被修改
- 修饰引用数据类型 表示其对其初始化以后便不能再能其指向另一个对象
8. 接口
- 包含的方法
- 抽象方法(只有函数的声明)
- 默认方法
- 静态方法
- 私有方法
- 接口只能使用
public
default
修饰,default是缺省值,表示具有包内访问权限控制 - 接口不能被实例化
- 接口的实现类 必须将接口中的抽象方法重写 否则 该类为抽象类
9.函数式接口
视频教程:狂神说
函数式接口(Function interface) 是java8新引入的这一种。
定义:任何一个接口,如果接口中只包含唯一一个抽象方法,那么就是函数式接口
特性:对于函数式接口,可以采用Lambda表达式来创建该接口的对象
为什么要引入lambda 表达式,还是为了简化代码,梳理一下 要调用一个接口的实现类 改进过程
- 在同一个包下,创建Main class ,接口,以及 接口的实现类 三个文件
- 在同一个.java文件下 ,在Main.class {}外部 创建 一个实现类 和接口
- 在同一个.java文件下,在Main.class{}内部 pswm 外部 创建一个静态外部类,接口还是在Main.class{}外部
- 在同一个.java文件下,在Main.class{}内部 pswm 内部 创建一个局部内部类,接口还是在Main.class{}外部
- 在同一个.java文件下,在Main.class{}内部 pswm 内部 创建一个匿名内部类,接口同上
- 使用Lambda表达式(代码如下)
示例:下面两个线程都是打印一句话
1
2
3
4
5
6
7
8
9
10
11
12
13public class LambdaPractice {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
System.out.println("匿名内部类实现");
}
}).start();
new Thread(() -> System.out.println("lambda表达式实现")).start();
}
}
10. 抽象类
抽象类 是一种抽象能力弱于接口的类
特征:
- 包含抽象方法的类 一定是抽象类
- 抽象类不一定包含 抽象方法
- 在抽象类中 还可以定义 成员变量 成员函数 构造方法 静态变量 静态方法
- 抽象类不能被实例化
11. 多线程编程
thread中.start() 与 .run()方法的区别?
.start():用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法 称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
.run(): run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class ThreadTest {
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
pong();
}
};
t.run();
System.out.println("主线程");
}
static void pong() {
System.out.println("子线程");
}
}
打印结果:
子线程
主线程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class ThreadTest {
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
pong();
}
};
t.start();
System.out.println("主线程");
}
static void pong() {
System.out.println("子线程");
}
}
打印结果:
主线程
子线程
.sleep()与.wait()的区别?
相同点:一旦执行方法,都可以使得当前的线程进入等待状态。
不同点:
函数声明的位置不同,sleep是在Thread类中,wait是在Object类中
关于是否可以指定睡眠时间,sleep函数必须指定,wait可以指定也可以不指定
slee() 会让当前正在运行的、用CPU时间片的线程挂起指定时间,休眠时间到自动苏醒进入可运行状态(阻塞->就绪->执行)
wait() 方法用来线程间通信,如果设置了时间,就等待指定时间;如果不设置,则该对象在其它线程被调用 notif()/ notifyAll()方法后进入可运行状态,才有机会竞争获取对象锁。
适用场景不同,sleep方法可以在任何需要的场景下调用,而wait方法必须在同步代码块中或者 同步方法中的监视器中调用
关于是否释放同步监视器,如果两方法都是使用在同步代码块或同步方法中,sleep()不会释放锁,wait会释放锁,并进入线程等待池。
sleep线程控制自身流程。wait用来线程间通信,使拥有该对象锁的线程等待直到指定时间或 notify
代码
执行sleep()方法 不会释放锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42public class ThreadSleepWait implements Runnable {
int number = 10;
public void firstMethod() throws Exception {
synchronized (this) {
number += 100;
System.out.println(number);
}
}
public void secondMethod() throws Exception {
synchronized (this) {
/**
* (休息2S,阻塞线程)
* 以验证当前线程对象的机锁被占用时,
* 是否被可以访问其他同步代码块
*/
Thread.sleep(2000);
number *= 200;
System.out.println(number);
}
}
public void run() {
try {
firstMethod();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
ThreadSleepWait threadSleepWait = new ThreadSleepWait();
Thread thread = new Thread(threadSleepWait);
thread.start();
threadSleepWait.secondMethod();
}
}
打印结果:
2000//先执行了secondMethod
2100执行wait()方法释放索
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42public class ThreadSleepWait implements Runnable {
int number = 10;
public void firstMethod() throws Exception {
synchronized (this) {
number += 100;
System.out.println(number);
}
}
public void secondMethod() throws Exception {
synchronized (this) {
/**
* (休息2S,阻塞线程)
* 以验证当前线程对象的机锁被占用时,
* 是否被可以访问其他同步代码块
*/
this.wait(2000);
number *= 200;
System.out.println(number);
}
}
public void run() {
try {
firstMethod();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
ThreadSleepWait threadSleepWait = new ThreadSleepWait();
Thread thread = new Thread(threadSleepWait);
thread.start();
threadSleepWait.secondMethod();
}
}
执行结果:
110//先执行secondMethod 但由于wait方法释放了锁 就会导致子线程得到资源 可以执行
22000
12. == 与 equals()
- ==是运算符、.equals()是方法(来自于父类Object)
- 使用==比较两个对象的内存地址是否相同。使用.equals()是比较两个对象的内容是否相同,比较值
- 如果没有重写.equals(),仍然比较的是地址
更加具体而言:
通过==比较基本数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public static void main(String[] args)
{
// integer-type
System.out.println(10 == 20);
// char-type
System.out.println('a' == 'b');
// char and double type
System.out.println('a' == 97.0);
// boolean type
System.out.println(true == true);
}
输出结果:
false
false
true
true通过==比较自定义数据类型
前提 需要保证两个对象的类型是相同的 或者 是父类子类之间的关系,否则会产生编译错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public static void main(String[] args)
{
Thread t = new Thread();
Object o = new Object();
String s = new String("GEEKS");
System.out.println(t == o);
System.out.println(o == s);
// Uncomment to see error
System.out.println(t==s);
}
输出结果:
false
false
//errorequals()
equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。
1
2
3public boolean quals(Object obj) {
return (this == obj);
}特殊情况 Stirng s=”abc” 与 String s=new String(“abc”)
String s=”abce”是一种非常特殊的形式,和new 有本质的区别。它是java中唯一不需要new 就可以产生对象的途径。以String s=”abce”;形式赋值在java中叫直接量,它是在常量池中而不是象new一样放在压缩堆中。这种形式的字符串,在JVM内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有有没有一个值为”abcd”的对象,如果有,就会把它赋给当前引用.即原来那个引用和现在这个引用指点向了同一对象,如果没有,则在常量池中新创建一个”abcd”,下一次如果有String s1 = “abcd”;又会将s1指向”abcd”这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象.而String s = new String(“abcd”);和其它任何对象一样.每调用一次就产生一个对象,只要它们调用。
也可以这么理解: String str = “hello”; 先在内存中找是不是有”hello”这个对象,如果有,就让str指向那个”hello”.如果内存里没有”hello”,就创建一个新的对象保存”hello”. String str=new String (“hello”) 就是不管内存里是不是已经有”hello”这个对象,都新建一个对象保存”hello”。
13. 集合
ArrayList
实现了List接口的可扩容数组(动态数组),基于数组实现
线程不安全容器,代替使用:
1
List list = Collections.synchronizedList(new ArrayList(...))
ArrayList扩容后的数组长度会增加50%(查看源码得知)
Vector
- Vector同 ArrayList一样,都是基于数组实现的,只不过 Vector是一个线程安全的容器,它对内部的每个方法都简单粗暴的上锁,避免多线程引起的安全性问题,但是通常这种同步方式需要的开销比较大,因此,访问元素的效率要远远低于 ArrayList
- 还有一点在于扩容上, ArrayList扩容后的数组长度会增加50%,而 Vector的扩容长度后数组会增加一倍。
LinkedList
双向链表实现,允许存储null
不安全的容器,必须加锁 或者 使用
1
List list = Collections.synchronizedList(new LinkedList(...))
Stack
LIFO 后进先出
继承自Vector 所以是线程安全的容器
构造方法
1
Deque<Integer> stack = new ArrayDeque<Integer>()
HashMap
HashSet是一个利用哈希表原理来存储元素的集合,允许存放null
非线程安全,HashTable是线程安全的容器
性能影响因素:初始容量以及加载因子
根据阿里巴巴Java开发手册上建议HashMap初始化时设置已知的大小,如果不超过16个,那么设置成默认大小16:
initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即loader factor)默认为0.75, 如果暂时无法确定初始值大小,请设置为16(即默认值)。
14. 泛型
泛型类
一个普通的类:
1
2
3class Father {
}泛型类:
1
2
3
4
5
6
7class Father<E> {
E sex;
public void add(E a) {
System.out.println(a);
}
}若实例化一个泛型类对象时,没有指定泛型的类型,会默认为Object类型
1
2
3
4
5//1.泛型类不指定类型时 默认是Object
Father father = new Father();
father.add("abc");
father.add(1);
father.add(1.2);若实例化一个泛型类对象时,指定了泛型的类型,那么 操作的数据必须时该类型
1
2
3
4//2.泛型类指定类型时, 类中的E 就都是该类型
Father<Integer> father1 = new Father<>();
father1.add(1);
// father1.add("2");报错继承自一个泛型类的情况
一个子类继承自一个泛型类父类
若指定了父类中 泛型的类型,子类就不需要是泛型类了
1
2
3
4//父类指定泛型
class Son2 extends Father<Integer> {
}1
2
3
4//3.父类指定泛型类型,继承的子类就不需要指定泛型类型了 可以直接使用
Son2 son2 = new Son2();
son2.add(1);
// son2.add("1");//报错若没有指定父类中的泛型的类型,子类就需要变成泛型类
1
2
3
4
5
6
7
8
9//5. 父类不指定泛型类 子类指定泛型类
Son<Integer> son1 = new Son<>();
son1.add(1);
// son1.add(1.0);//报错
//父类不指定泛型类 子类也不指定泛型类,那么E就默认是Object
Son son3 = new Son();
son3.add(1);
son3.add(1.0);
son3.add("1");细节
<>不一定只有一个泛型,可以有多个
泛型类的构造器的写法,不带泛型类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Father<E> {
E sex;
//构造器错误写法
public Father<E>(){
}
//构造器正确写法
public Father(){
}
public void add(E a) {
System.out.println(a);
}
}不同的泛型类型 但是是同一个集合类 ,不可以相互赋值
1
2
3List<Integer> list = new ArrayList<>();
List<String> list2 = new ArrayList<>();
// list = list2;//报错泛型类中的静态方法 不允许有泛型参数
这是因为静态方法是随着类的加载而被加载,先于对象存在,而泛型的参数是实例化对象时才去指定,也就是加载顺序不同
不能直接使用E[] 来创建数组
1
2// E[] arr = new E[10];//错误
E[] arr = (E[]) new Object[10];
泛型方法
什么是泛型方法?
并不是形参当中带了 泛型的参数就是泛型方法了 ,要求这个方法的泛型的参数类型要与当前类的泛型参数无关
1
2
3
4
5
6
7
8
9
10
11public class TestGeneric02<E> {
//这并不是泛型方法
public void a(E m) {
}
//这才是泛型方法,T的确定是在调用这个方法的时候 才确定下来
public <T> void b(T n) {
}
}泛型方法对应的那个泛型参数类型 和 当前 所在的这个类是否是泛型类,泛型是啥 无关,下面这个类不是泛型类 但仍然可以包含泛型方法
1
2
3
4
5
6public class TestGeneric02 {
public <T> void b(T n) {
}
}泛型方法可以是静态方法,泛型类中的带了泛型类型的方法不能是静态方法
这是因为泛型方法中 泛型参数与所在类是否是泛型类 泛型的参数无关。
虽然静态方法优先于对象的加载,但是泛型方法的泛型类型是在调用时才被确定的
泛型参数中的继承关系
多态的一种体现:父类指针 指向子类对象,如下面代码块1
但是 在下面的代码块2 String 虽然是Object类的子类 但是 List
与 List 1
2
3
4
5
6
7Object[] objects = new Object[10];
Object[] objects1 = new Object[10];
String[] strings = new String[10];
String[] strings1 = new String[10];
objects = strings;//多态的一种体现 父类-》子类
objects = objects1;
strings = strings1;1
2
3
4List<String> stringList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
// objectList = stringList;//报错泛型受限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/*泛型的上限 定义了上限
List<? extends Person> 是 List<Person> 的父类 以及 List<Person子类> 的父类
如果想要使用 父类 = 子类对象 子类类型不能超过 Person
*/
List<? extends Person> list = null;
// list = objectList;报错
list = personList;//正确 父类 ->子类
list = studentList;//正确 父类 ->子类
/*泛型的下限
List<? super Person> 是 List<Person> 的父类 以及List<Person 父类> 的 父类
*/
List<? super Person> list1 = null;
list1 = objectList;//正确 父类 ->子类
list1 = personList;//正确 父类 ->子类
// list1 = studentList;//报错泛型通配符
List<?> list
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class Test02 {
/*
以下 前三个方法冲突 List<Object> List<String> List<Integer> 是同级关系 相当于一个方法了
*/
public void a(List<Object> list) {
}
public void a(List<String> list) {
}
public void a(List<Integer> list) {
}
public void a(List<?> list){
for(Object o:list){
System.out.println(o);
}
}
}