Java基础知识复习

安全岗位也太少了,很多都是开发岗,也来复习(预习)一下Java的基础知识,以备找不到安全岗位去转开发。

Java基础概念与常识

Java语言的特点

  1. 面向对象(继承、多态、封装)
  2. 平台无关
  3. 安全可靠
  4. 支持多线程和方便的网络编程
  5. 编译与解释共存(.class->机器码这一步JVM类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了JIT编译器,而JIT属于运行时编译。当JIT编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于Java解释器的。这也解释了我们为什么经常会说Java是编译与解释共存的语言。)

import java 和 javax的区别

一开始JavaAPI所必需的包是java开头的包,javax是当时的扩展包,后来javax成为JavaAPI的组成部分,由于将javax移动到java很麻烦,因此将javax包也成为了标准API的一部分

Java基本类型占存储空间大小

Java中常见的关键字

hashCode() 与 equals()

equals方法

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。

1
2
3
4
//equals方法实现的源码
public boolean equals(Object obj) {
return (this == obj);
}

因此通常情况下,我们要判断两个对象是否相等,一定要重写 equals 方法。

hashCode方法

hashCode 翻译为中文是散列码,它是由对象推导出的一个整型值,并且这个值为任意整数,包括正数或负数。需要注意的是:散列码是没有规律的。如果 x 和 y 是两个不同的对象,x.hashCode() 与 y.hashCode() 基本上不会相同;但如果 a 和 b 相等,则 a.hashCode() 一定等于 b.hashCode()。

1
2
//hashCode 在 Object 中的源码如下
public native int hashCode();

从上述源码可以看到,Object 中的 hashCode 调用了一个(native)本地方法,返回了一个 int 类型的整数,当然,这个整数可能是正数也可能是负数。

相等的值hashCode一定相等,不同的值hashCode也有可能相等

为什么重写equals方法一定要重写hashCode方法

举一个Set的例子:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class EqualsToListExample {
public static void main(String[] args) {
// 对象 1
Persion p1 = new Persion();
p1.setName("Java");
p1.setAge(18);
// 对象 2
Persion p2 = new Persion();
p2.setName("Java");
p2.setAge(18);
// 创建 Set 对象
Set<Persion> set = new HashSet<Persion>();
set.add(p1);
set.add(p2);
// 打印 Set 中的所有数据
set.forEach(p -> {
System.out.println(p);
});
}
}
class Persion {
private String name;
private int age;

@Override
public boolean equals(Object o) {
if (this == o) return true; // 引用相等返回 true
// 如果等于 null,或者对象类型不同返回 false
if (o == null || getClass() != o.getClass()) return false;
// 强转为自定义 Persion 类型
Persion persion = (Persion) o;
// 如果 age 和 name 都相等,就返回 true
return age == persion.age &&
Objects.equals(name, persion.name);
}

@Override
public int hashCode() {
// 对比 name 和 age 是否相等
return Objects.hash(name, age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Persion{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

重写hashCode方法后,运行结果为:

1
2
java EqualsToListExample
Persion{name='Java', age=18}

注释掉重写部分,运行结果为:

1
2
3
java EqualsToListExample
Persion{name='Java', age=18}
Persion{name='Java', age=18}

出现以上问题的原因是,如果只重写了 equals 方法,那么默认情况下,Set 进行去重操作时,会先判断两个对象的 hashCode 是否相同,此时因为没有重写 hashCode 方法,所以会直接执行 Object 中的 hashCode 方法,而 Object 中的 hashCode 方法对比的是两个不同引用地址的对象,所以结果是 false,那么 equals 方法就不用执行了,直接返回的结果就是 false:两个对象不是相等的,于是就在 Set 集合中插入了两个相同的对象。

但是,如果在重写 equals 方法时,也重写了 hashCode 方法,那么在执行判断时会去执行重写的 hashCode 方法,此时对比的是两个对象的所有属性的 hashCode 是否相同,于是调用 hashCode 返回的结果就是 true,再去调用 equals 方法,发现两个对象确实是相等的,于是就返回 true 了,因此 Set 集合就不会存储两个一模一样的数据了,于是整个程序的执行就正常了。

装箱与拆箱

装箱就是 自动将基本数据类型转换为包装器类型;拆箱就是 自动将包装器类型转换为基本数据类型。

装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。(xxx代表对应的基本数据类型)。

注意:在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。创建Boolean对象的时候,直接返回定义好的静态成员属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
public static void main(String[] args) {

Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;

System.out.println(c==d);
System.out.println(e==f);//false
System.out.println(c==(a+b));
System.out.println(c.equals(a+b));
System.out.println(g==(a+b));
System.out.println(g.equals(a+b));//false
System.out.println(g.equals(a+h));
}
}

当 “==”运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。另外,对于包装器类型,equals方法并不会进行类型转换。

重载与重写

重载

重载就是同样一个方法能够根据输入数据的不同,做出不同的处理;Java允许重载任何方法,而不止是构造器方法,因此要完整描述一个方法时需要指出方法名和参数类型,这叫做方法的签名(返回类型不是方法签名的一部分)。

重写

重写就是当子类继承父类的相同方法时,要求输入数据一样,但是要做出与父类不同的响应时,就需要覆盖父类方法。

重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

  1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
  2. 如果父类方法访问修饰符为private/final/static则子类就不能重写该方法,但是被static修饰的方法能够被再次声明。
  3. 构造方法无法被重写

Java面向对象

面向对象和面向过程的区别

面向过程的性能一般比较好,因为类的调用需要实例化,开销比较大;但是面向过程没有面向对象那样容易维护、易复用、易扩展。

构造器可否被override?

构造器不可以被重写,但是可以被重载,一个类中可以有多个构造函数(因为可以重载)。

在Java中构造一个空的并且没有参数的构造方法的作用

Java在执行子类构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中的“没有参数的构造方法”(帮助子类做初始化工作)。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中有没有用super()来调用父类的构造方法,则编译时会发生错误(实际参数列表和形式参数列表长度不同),因为Java程序在父类中找不到没有参数的构造方法可供执行。

成员变量与局部变量的区别?

  1. 成员变量属于类,局部变量是方法中定义的变量或是方法的参数;成员变量可以被public、private、static修饰,局部变量不能被访问控制修饰符及static修饰
  2. 从变量在内存中存储方式来看:静态成员变量属于类(静态成员变量存储在堆的永久生成区域中),其余的属于实例,对象都是存储在堆内存,局部变量存储在栈内存。
  3. 在内存的生存时间:成员变量是对象的一部分,随着对象的存在而存在,局部变量随着方法调用自动消失。
  4. 成员变量如果没有赋值会默认赋值(final修饰的必须显式赋值),局部变量则不会自动赋值。

注:new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(引用存放在栈内存中)

构造方法的特性

  1. 名字与类名相同
  2. 没有返回值,但是不能用 void 声明构造函数
  3. 生成类的对象时自动执行,无需调用

面向对象的三大特征

封装

封装是指把对象的状态信息(属性)隐藏在对象内部,不允许外部对象直接访问对象内部的信息,但是可以提供一些可以被外界访问的方法来操作属性。

多态

同一个类实例的相同方法在不同情形下有不同的表现形式

  1. 方法多态:① 方法重载:同一个方法名称可以根据参数的类型或个数不同调用不同的方法体 ② 方法覆写:同一个父类的方法可以根据实例化子类的不同也有不同的实现
  2. 对象多态:向上转型(将子类对象变成父类对象)和向下转型(强转,子类 子类实例 = (子类)父类实例)

继承

继承是使用已存在的类定义作为基础建立新的类的技术,新的类可以增加新的数据或新的功能,也可以使用父类的功能,但不能选择性的继承父类。通过使用继承,可以快速创建新的类,可以提高代码的重用,程序的可维护,提高开发效率

子类拥有父类所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类无法访问,只是拥有;子类可以拥有自己属性和方法,可扩展;子类可以用自己的方式实现父类的方法

修饰符

静态方法与实例方法的区别:外部调用静态方法时,可以使用类名.方法名的形式,也可以用对象名.方法名的形式,实例类只有后者;静态方法访问本类成员时只允许访问静态成员,不允许访问实例成员变量和实例方法

super

  1. super.XXX 这里的XXX指的是父类的成员变量名即父类的属性,或者对象名

  2. super.XXX( ) 这里的XXX是父类中的其中一个方法名

  3. super( ) 这种形式指的是:调用父类没有参数的构造方法(也叫构造函数)(注意: 这里super( ) 只能放在子类的构造方法里面,并且只能放在构造方法的首句)

  4. super( x,y,z…) 此形式指:调用父类有参数的构造方法,也必须放在子类的构造方法(成员方法不可以)里面,并且只能放在构造方法的首句。其中x,y,z是指的与父类此有参构造方法中参数数据类型相对应的子类中的参数

final

  1. final修饰变量的特性

final修饰的变量,这个变量是不可修改的。

这里又分为两个情况,分别是值不可修改和引用不可修改。

如果变量是值变量,那么该变量的值不能修改,比如定义了一个整型对象是final修饰的,那么这个对象开始值是多少就固定下来了,不 允许修改了,这个类似C语言里面的const。

如果变量是引用变量,比如定义了学生对象student并修饰为final的,那么student的年龄、姓名都可以修改,但是,不允许再给student赋值新的对象了,比如再执行student =new Student()构造方法,这是不允许的。

  1. final修饰方法的特性

final修饰后的方法,禁止子类继承的时候重写方法。

同时final修饰后的方法执行速度会更快,因为final修饰的类似C里面的内联机制,在调用方法的时候,直接将方法插入到方法调用的位置,这样方法调用就减少了寻址调用的时间开销。

  1. final修饰类的特性

final修饰后的类,是禁止被继承的。

this

  1. this.data; //访问属性

  2. this.func(); //访问方法

  3. this(); //调用本类中其他构造方法

接口和抽象类的区别

  1. 接口方法默认为public,jdk7或者更早版本中,接口里面只能有常量变量和抽象方法,并且接口方法必须由选择实现接口的类实现,所有方法在接口中不能有实现,jdk8的时候接口可以有默认方法和静态方法,jdk9在接口中引入了私有方法和私有静态属性;抽象类可以有非抽象方法
  2. 接口中只能由static、final变量,不能有其他变量,抽象类中不一定
  3. 一个类可以实现多个接口,但是只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口
  4. 接口方法默认修饰符为public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写,所以不能用private)
  5. 抽象是对类的抽象,是一种模板设计,接口是对行为的抽象,是一种行为规范

String、StringBuilder和StringBuffer

String类中使用final关键字修饰字符数组来保存字符串,private final char[] value(Java 9 之后改用byte数组存储,即byte[] value),因此String对象是不可变的。

而StringBuilder与StringBuffer都继承自AbstractStringBuilder类,其中字符串数组没有用final关键字修饰,因此这两种对象都是可变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
}

线程安全性

String中对象是不可变,因此线程安全。

StringBuffer方法中添加了同步锁,因此是线程安全的

StringBulider没有对方法添加同步锁,因此是非线程安全的

性能

StringBuffer:可变字符串、效率低、线程安全;

StringBuilder:可变字符序列、效率高、线程不安全;

总结

操作少量数据用String

单线程操作字符串缓冲区下大量数据使用StringBuilder

多线程操作字符串缓冲区下大量数据使用StringBuffer

Java序列化时有些字段不想序列化怎么办

对于不想进行序列化的变量可以使用transient(暂存的)关键字修饰,该关键字可以阻止实例中使用此关键字修饰的变量的序列化

获取键盘输入的常用方法

Scanner

1
2
3
Scanner input = new Scanner(System.in);
String in = input.nextLine();
input.close();

BufferReader

1
2
3
4
5
6
7
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String in = null;
try {
in = input.readLine();
} catch (IOException e) {
throw new RuntimeException(e);
}

Java核心技术

Collections工具类常见方法总结

排序等操作

方法

1
2
3
4
5
6
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。

示例代码

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
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class testCollections {
public static void main(String[] args){
int[] nums = {1,2,1,4,4,7,1,5,4,9,4};
List<Integer> numsList = Arrays.stream(nums).boxed().collect(Collectors.toList());
System.out.println("源字符列表:");
System.out.println(numsList);
Collections.reverse(numsList);
System.out.println("反转字符列表:");
System.out.println(numsList);
System.out.println("排序字符列表:");
Collections.sort(numsList);
System.out.println(numsList);
System.out.println("shuffle字符列表:");
Collections.shuffle(numsList);
System.out.println(numsList);
System.out.println("自定义顺序字符列表:");
Comparator<Integer> reComp = (n1,n2) -> n2 - n1; // lambda
Collections.sort(numsList, reComp);
System.out.println(numsList);
System.out.println("交换开头和结尾字符列表:");
Collections.swap(numsList,0,10);
System.out.println(numsList);
System.out.println("旋转字符列表:");
Collections.rotate(numsList, 5); //当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。
System.out.println(numsList);
}
}

查找替换

1
2
3
4
5
6
7
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比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,类比int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素

Arrays工具类常见方法总结

  • Arrays位于java.util包下(Arrays是一个操作数组的工具类)
  • Arrays包含各种操作数组的方法(例如排序和搜索)。该类还包含一个静态工厂,允许将数组视为列表。
  • Arrays类里的方法都是静态方法可以通过Arrays.方法名()直接调用
1
2
3
4
5
6
7
Arrays.sort() //排序 
Arrays.binarySearch() // 查找
Arrays.equals() //比较
Arrays.fill() // 填充
Arrays.asList() //转列表
Arrays.toString() //转字符串
Arrays.copyOf() //复制

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.Arrays;
import java.util.List;

public class testArrays {
public static void main(String[] argv){
String[] s = {"a","b","c","a","f","b"};
String[] s1 = Arrays.copyOf(s,s.length);
List<String> list = Arrays.asList(s);
System.out.println(Arrays.toString(s));
Arrays.sort(s);
System.out.println(Arrays.toString(s));
System.out.println(Arrays.binarySearch(s, "c"));
System.out.println("Arrays.equals(s, s1):" + Arrays.equals(s, s1));
}
}

异常

异常类层次结构:

Java中所有的异常都有一个共同的祖先java.lang包中的Throwable类,该类有两个重要的子类:Exception和Error,二者都是Java异常处理的重要子类,各自都包含大量子类。

Error是程序无法处理的错误,表示运行应用程序中较严重的问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时Java虚拟机出现的问题。

Exception是程序本身可以处理的异常。

Throwable类常用的方法

1
2
3
4
public String getMessage() //返回异常发生时的简单描述
public void printStackTrace() //在控制台打印Throwable对象封装的异常信息
public String toString() // 返回异常发生时的详细信息
public String getLocalizedMessage() // 返回异常对象的本地化信息

try-catch-finally

1
2
3
4
5
6
7
8
9
10
11
try{
//some code may have some exception
}catch( RuntimeException e ){ // catch some exception
//do something
}catch( IOException e ){ // catch another exception
// do something
}
...
finally{
// both catch and not catch will do something
}

以下四种特殊情况下finally不会被执行:

  • finally语句第一行发生了异常
  • 在前面的代码中用了System.exit(int)已经退出程序。
  • 程序所在的线程死亡
  • 关闭CPU

注意:当try语句和finally语句中都有return语句时,在方法返回之前,finally的语句将被执行,并且finally的语句的返回值将会覆盖原始的返回值

try-with-resources

1
2
3
4
5
6
7
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}

等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//读取⽂本⽂件的内容
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}

通过使用分号分隔,可以在try-with-resources块中声明多个资源

多线程

Java线程状态:

线程状态变迁:

线程创建之后处于NEW状态,调用start方法之后开始运行,线程这时候处于READY状态,可运行状态获得cpu时间片之后就处于RUNNING状态,操作系统对Java虚拟机隐藏READY和RUNNING状态,只能看到RUNNABLE状态。

当线程执行到wait方法之后,线程进入WAITING状态,进入等待状态的线程需要依靠其他线程的通知才能返回到运行状态,TIMED_WAITING状态相当于在等待基础上增加了超时限制,比如通过sleep方法或者wait(long millis)方法可以将Java线程置于TIMED_WAITING状态。当超时时间结束后重回RUNNABLE状态。当线程调用同步方法时,在没有获取到锁的情况下,线程会进入BLOCKED状态。线程执行完毕会进入TERMINATED状态。

文件与IO

Java中的IO流分类

  • 按照流向分为输入流和输出流
  • 按照操作单元分为字节流和字符流
  • 按照流的角色分为节点流和处理流

InputStream/Reader 字节输入流/字符输入流

OutputStream/Writer 字节输出流/字符输出流

为什么要分字节流和字符流

字符流实际上可以由Java虚拟机将字节转换得到,但是这个过程比较耗时并且容易出现乱码问题,因此IO流就提供了一个直接操作字符的接口,方便平时对字符进行流操作

BIO、NIO、AIO的区别

  • BIO是同步阻塞IO模式,数据的读取和写入必须阻塞在一个线程内等待其完成。适用于低并发、低负载的情况
  • NIO是同步非阻塞IO模式,支持面向缓冲的,基于通道的IO方法,适用于高负载、高并发的应用
  • AIO是NIO的改进版,是异步非阻塞的IO模型。异步IO是基于时间和回调机制实现的,也就是应用操作之后会直接返回,当后台任务处理完成后系统会通知相应的线程执行后续的操作

Java基础知识复习
https://chujian521.github.io/blog/2023/02/18/Java基础知识复习/
作者
Encounter
发布于
2023年2月18日
许可协议