11月面试复习

写在前面:JAVA神书:

《java编程思想》(Thinking in Java)

《java核心技术》

Spring是业务层框架

WEB层 业务层 持久层

SpringMvc Spring SpringJDBC

这些都是Spring的一部分,Spring一站式开发,他在所有层面都给你提供了解决方案。

IOC Di 为了解耦

实现过程:

使用者 (工厂 XML配置文件) 资源

AOP

jar.exe 打jar包:day11 07

  • maven clean
  • maven compile
  • maven test
  • maven package
  • maven install
  • maven deploy

Java语言基础

基本类型

JAVA中8中基本数据类型

1
2
3
4
byte short int long    float double    char    boolean
1字节 2 4 8 4 8 2 1
8位 = 1字节
8bit(b) = 1Byte(B)

默认类型和转换问题

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
		/**
* java中的数字,没有小数点,默认为int,比如 1,2,3,999这些
* 有小数点,默认为double,比如3.4,5.666666,7.0这些
* long l = 9999999 错误:9999999是int,但是
*/
//报错
float f = 5.0;//有小数点的都默认是double,高精度位double不能向低精度位float转换

//正确
float f2 = (float) 3.4;
float f3 = 3.4f;
float f4 = 3.4F;
double b = 3.4;
double b2 = 3.4F;//正确,因为低精度位可以默认向高精度位强转换。


long l2 = 9999999999; //报错,99亿超过了int的最大值(21亿多)
long l = 4; //正确,因为低精度位int可以默认向高精度位long强转换。
long l3 = 9999999999L; //正确,转为了long类型



int i += 1; //错误 i = i + 1; 赋值符号先从右边计算,i + 1,此时i还没有初值,所以错误。
short s = 1;
s += 1; //正确,相当于 s = (short) (s+1); +=符号 会自动帮你强转为左边的类型
s = s + 1; //错误,s是short类型,1是int类型,低精度与高精度做运算时,会转换为高精度,这里转换为int,然后将int类型赋值给short类型,无法将低精度直接赋值给高精度,错误,需要手动强转。

位逻辑运算符

计算时,先把十进制数字转换为二进制数字

与(& and)、或(| or)、非(~)、 异或(^)

详情:https://www.cnblogs.com/lichengze/p/5713409.html

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
package Test;

public class Test1 {
public static void main(String[] args) {
//与 & :均为1时为1,有一个为0就为0
/*
int a=129;//00000000 00000000 00000000 10000001
int b=128;//00000000 00000000 00000000 10000000
//00000000 00000000 00000000 10000000
System.out.println("a&b 的结果是:"+(a&b));//128
*/

//或 | :有一个为1即为1,均为0时为0
/*
int a=129;//00000000 00000000 00000000 10000001
int b=128;//00000000 00000000 00000000 10000000
//00000000 00000000 00000000 10000001
System.out.println("a|b 的结果是:"+(a|b));//129
*/

//非~ : 0变为1,1变为0
/*
int a=2;//00000000 00000000 00000000 00000010
//11111111 11111111 11111111 11111101
System.out.println("~a 的结果是:"+(~a));
System.out.println(Integer.toBinaryString(-3));
//11111111 11111111 11111111 11111101
*/

//异或^ : 相同则为0,不同则为1
int a=8;//00000000 00000000 00000000 00001000
int b=2;//00000000 00000000 00000000 00000010
//00000000 00000000 00000000 00001010
System.out.println("a^b 的结果是:"+(a^b));//10
}
}

逻辑运算符

&& || (短路与、短路或)

位移运算符

计算2*8最快的方式:8<<1

1
2
3
4
5
6
7
8
int i = 8;
System.out.println(i<<1);//8*2=16 左移 *2 右移 /2
System.out.println(i>>2);//8/4=2 右移 /2的2次方
System.out.println(i<<3);//8*8=64 左移 *2的3次方

//左移一位
//00000000 00000000 00000000 00001000
//00000000 00000000 00000000 00010000

无符号位移

无符号右移:>>> 无符号左移: <<<

即无论正负数,右移之后符号位均补 0

计算-2 >>> 1结果是多少

1
2
3
4
5
6
-2 的二进制求法是正数取反加1

2 的二进制表示为 00000000 00000000 00000000 00000010
因此-2的二进制表示为 11111111 11111111 11111111 11111110

右移1位,其余补0 011111111 11111111 11111111 1111111

关键字

  • transient 修饰的变量不参与序列化。例:Map中的entrySet变量
  • native 与C++联合开发时使用,使用此关键字说明这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。例如:Object的hashCode()getClass()等。参考:https://www.cnblogs.com/KingIceMou/p/7239668.html
  • volatile标识修饰的变量随时可能被修改,所修饰变量的值被修改后会立即被写入主存。可见性,有序性,不具备原子性,是一种比sychronized更轻量级别的同步机制。

输出流

1
2
> System.out和System.err,为 标准输出流 和 标准错误输出流
>

out存在缓冲区;err不存在缓冲区,只要有内容立即输出。【然而目前的验证发现并不是这样,err在out上面时,也存在先输出out,之后才输出err的情况】

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
package IO;

public class SystemOutTest
{
static
{
System.out.println("1");
}
{
System.out.println("2");
}
public SystemOutTest()
{
System.err.println("3");
}

public static void main(String[] args) {
//main方法所在的类会先被加载
//1、加载SystemOutTest,执行clinit(),只执行一遍
//clinit()由静态代码块,静态变量赋值代码组成,顺序按照从上到下执行
//2、new一个对象时,会执行init()
//init()由非静态代码块,非静态变量赋值代码 和构造函数组成,前面两个按照顺序从上到下执行,构造函数一定最后执行

//所以按理说,应该是先输出1(clinit()),然后输出2最后输出3(init())
//但是由于err是没有缓冲的,out存在缓冲,所以3的输出是随机的。1、2输出顺序固定(都在缓冲区,1较之2先输出)
//可能的输出 3 1 2 1 3 2 1 2 3
new SystemOutTest();

//如果将out都改为err;或者将err改为out,那么一定会输出1 2 3
}
}

内部类

参考连接:https://blog.csdn.net/weixin_44929171/article/details/90318941

将类定义在一个类内部,即成为了内部类。内部类产生的class文件名为“外部类$内部类”

成员内部类、局部内部类、匿名内部类、静态内部类。

成员内部类

成员内部类中不能定义static变量和static方法

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
package JBasic;

/**
* 成员内部类
*/
public class OuterClass {
private int x;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}

public int func(){
return x+2;
}

//成员内部类,就像这个类的成员
public class InnerClass{
public int a;
public int func(){
return a+1;
}
}

public static void main(String[] args) {
OuterClass out = new OuterClass();
out.setX(3);
System.out.println(out.func());

InnerClass in = out.new InnerClass();
in.a = 1;
System.out.println(in.func());
}
}
局部内部类

在类的方法中,作为局部的类,不能被任何其他地方引用。就像局部变量一样,因此不能有任何访问修饰符

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
package JBasic;

/**
* 局部内部类
*/
public class PartClass {
public int x;

public int func(){
//局部内部类,就像局部变量
class InnerClass{
public int func(){
System.out.println("局部内部类...");
return 1;
}
}
int func = new InnerClass().func();
return x + func;
}

public static void main(String[] args) {
PartClass part = new PartClass();
part.x = 5;
System.out.println(part.func());
}
}
匿名内部类

类的定义和创建一起完成,目的是创建一个只使用一次的类的实例。作用是为了简化书写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package JBasic;

/**
* 匿名内部类
*/
abstract class Inner{
abstract void show();
}
public class AnonyClass {
public void func(){
new Inner(){
@Override
void show() {
System.out.println("匿名内部类中方法的执行");
}
}.show();
}

public static void main(String[] args) {
AnonyClass anony = new AnonyClass();
anony.func();
}
}
静态内部类

与类的其他成员相似,静态内部类使用static修饰,也称嵌套类

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
package JBasic;

/**
* 静态内部类
*/
public class StaticClass {
public static int x = 3;
//静态内部类
static class InnerClass{
int a = 1;
static int b = 2;

public int func(){
return a + x;
}

public int func2(){
return b + x;
}
}

public static void main(String[] args) {
System.out.println(InnerClass.b);//2

InnerClass inner = new InnerClass();
System.out.println(inner.func2());//5
}
}
  • 静态内部类中可以定义静态成员,而成员内部类不能;
  • 静态内部类只能访问外层类的静态成员,成员内部类可以访问外层类的实例成员和静态成员;
  • 创建静态内部类的实例不需要先创建一个外层类的实例;相反,创建成员内部类实例,必须先创建一个外层类的实例。
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
interface Entry<K,V> {

K getKey();

V getValue();

V setValue(V value);

boolean equals(Object o);

int hashCode();

public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}


public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}


public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}


public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
}

原生命令编译java代码

D盘下存在一文件:Hello.java

1
2
3
4
5
6
7
package com.insist.demo;

public class Hello{
public static void main(String[] args){
System.out.println("hello world!");
}
}

在D盘根路径唤出cmd命令行,执行命令javac Hello.java,该命令表示用java编译器编译Hello.java文件,编译成功,什么都不显示。生成字节码文件Hello.class

执行命令java Hello,该命令表示用java虚拟机解释并执行Hello类,发现出错:错误: 找不到或无法加载主类 Hello。

原因:一个类的全名是包名+类名,所以类Hello的全名是:com.insist.demo.Hello

javac命令不会出错是因为javac编译的是文件,存在Hello.java这个文件,所以不出错。

java命令出错是因为不存在Hello这个类。

解决方案:在D盘下新建com/insist/demo文件夹(三个文件夹),将Hello.class放到com/insist/demo文件夹下,然后在D盘根路径唤出cmd命令行,执行java com.insist.demo.Hello即可成功,打印出hello world

类、对象的加载过程

main方法所在的类会先被初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Father.java:
public class Father{
xxx
}

Son.java:
public class Son extends Father{
xxx

public static void main(String[] args){
Father f = new Father();
}
}
虽然main()中的代码是先对Father进行实例化得到f,按理说应该先对Father类初始化,再进行f的实例初始化。但是main方法所在的类会先被初始化,所以Son类会先被初始化。又因为Son是Father的子类,子类初始化需先初始化父类,所以还是会先对Father类初始化。

类初始化过程:

类被初始化的触发条件:第一次用到该类,就可以说是第一次有new他的对象的时候,会先进行类初始化。

还有就是main方法所在的类会先进行初始化

执行clinit(),该方法由静态类变量显示赋值代码静态代码块组成,从上到下顺序执行。子类初始化需先初始化父类。只执行一次clinit()。

clinit()是虚拟机帮我们生成的,在类得字节码文件(class文件)中能够看到。classinit

实例初始化过程:

执行init(),init()可能有多个,有几个构造方法就有几个init(),该方法由非静态类变量显示赋值代码非静态代码块(从上到下顺序执行)和对应构造函数代码(最后执行)组成。init()的首行一定是super()或super(实参列表),即对应父类的init()。每次创建一个对象,都要执行一次init()。

哪些方法不能被重写?

final方法

静态方法

private修饰的子类不可见的方法

Father.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package hx.insist.com.Demo;

public class Father {
private int i = test();
private static int j = method();

static{
System.out.println("1");
}
Father(){
System.out.println("2");
}
{
System.out.println("3");
}
public int test(){
System.out.println("4");
return 1;
}
public static int method(){
System.out.println("5");
return 1;
}
}

Son.java

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
package hx.insist.com.Demo;

public class Son extends Father {
private int i = test();
private static int j = method();
static {
System.out.println("6");
}
Son(){
System.out.println("7");
}
{
System.out.println("8");
}
public int test(){
System.out.println("9");
return 1;
}
public static int method(){
System.out.println("10");
return 1;
}

public static void main(String[] args) {
Son s1 = new Son();
System.out.println();
Son s2 = new Son();
}
}

分析:存在main()方法,先对main()所在的类进行初始化,执行Son()的clinit()。Son是Father的子类,所以先执行Father的clinit(),再执行Son的clinit()。

Father的clinit()由静态变量显示赋值代码静态代码块组成,就是这些:

1
2
3
4
5
private static int j = method();

static{
System.out.println("1");
}

执行静态变量j的显示赋值代码时,会执行method(),输出5,此时子类存在同名的静态方法,但是静态方法是不能被子类重写的,所以这个地方还是会执行父类自己的静态方法method()。

然后输出1

Son的clinit()由静态变量显示赋值代码和静态代码块组成,就是这些:

1
2
3
4
private static int j = method();
static {
System.out.println("6");
}

和上面一样,执行method(),输出10。然后输出6

以上就是在发现main()方法时,进行的动作:进行main()方法所在类的初始化(类Son的初始化),然后由于Son是子类,所以要先执行其父类Father的初始化。

接着开始执行代码:Son s1 = new Son();

执行这句代码时要用到类Son,所以要先初始化类,由于之前类Son已经初始化过了,一个类只会被初始化一次,所以不再进行初始化,这里new一个对象,就是进行了对象的初始化(或称实例初始化)。对象初始化会执行对象的init()方法。init()由super()非静态变量显示赋值代码非静态代码块对应构造函数组成,super()就是执行父类的init()。所以先执行Father的super(),非静态变量显示赋值代码,非静态代码块,对应构造函数。然后再执行子类Son的非静态变量显示赋值代码,非静态代码块,对应构造函数。

Father的super()是执行Object类的init(),不会由输出,不管。

Father的非静态变量显示赋值代码,非静态代码块,对应构造函数是这些:

1
2
3
4
5
6
7
private int i = test();
Father(){
System.out.println("2");
}
{
System.out.println("3");
}

非静态变量显示赋值代码非静态代码块从上到下顺序执行,对应构造函数一定是最后执行。所以先执行i的赋值语句i = test(),这句代码会执行test(),但是子类也同时存在一个同名的重写函数test(),又由于父类的test()是public修饰的普通函数,所以test()会被子类覆盖,执行为子类的test()。【也可以这样理解:执行非静态方法时,前面都有一个默认的对象this,执行test(),其实执行的是this.test()。而this代表当前对象,当前正在创建Son对象,所以this代表Son对象,执行this.test()就是执行Son的test()。】这里是不是多态还有待考究,因为多态一定要是基类指针指向子类对象,而这里的this指针到底是代表Father还是Son,我还不清楚,我感觉是Son。如果this指针是指向Son的话,这里就不是多态了,用派生类的指针调用派生类的方法,当然是理所应当的。只有当使用基类指针:指向Father的指针调用test()时,才会产生调用自己的test()还是调用被子类重写的test()这个问题,答案是调用被重写的test(),这样才是态。

综上,会先调用子类的test(),输出9,然后输出3,最后输出2

接着执行子类的init():

1
2
3
4
5
6
7
private int i = test();
Son(){
System.out.println("7");
}
{
System.out.println("8");
}

执行i = test(),调用子类的test()方法,输出9,然后输出8,最后输出7

所以会先输出:5 1 10 6 9 3 2 9 8 7

然后执行System.out.println();换行。

最后执行Son s2 = new Son();。没new一个对象就会进行一次对象初始化(或称实例初始化),所以还会再执行一次上述对象初始化过程,输出:9 3 2 9 8 7

字符串

String、StringBuffer、StringBuilder

三者在执行速度方面的比较:StringBuilder > StringBuffer > String

StringBuilder比StringBuffer块的原因:StringBuilder线程不安全,单线程使用较为合适。

StringBuilder和StringBuffer都比String块的原因:String是字符串常量,其他两个是字符串变量。

StringBuffer多线程安全的原因:在许多方法上添加了synchronized锁

对于三者使用的总结: 1.如果要操作少量的数据 = String

           2.单线程操作字符串缓冲区 下操作大量数据 = StringBuilder

           3.多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

为什么把String,StringBuffer,StringBuilder都设计称final的,而其他类比如ArrayList,就不是final的?

主要是为了”效率“和”安全性“,如果指定一个类为 final,则该类所有的方法都是 final。java 编译器会寻找内联所有的 final 方法。 此举能够使性能平均提高 50%。(咱也不懂啥意思)

重载与重写,多态

继承一个类,把父类的某个方法重新实现一遍,就叫重写(Override);重写过程中,方法名,参数,返回类型必须和父类保持一致。【重写是父类的方法不能满足现有需求时发生的】

同一个类中,相同的方法名,参数类型不同,或参数个数不同,就叫重载(Overload)。(返回值不同不能作为重载的判断条件)【重载是一个类本身想要满足不同场景时发生的,因为重载本身的参数就不同,因此在硬编码时期就已经可以通过参数判断调用的是哪个方法,所以重载和多态无关】

多态三要素:

1、继承

2、重写(子类重写父类方法)

3、父类指针指向子类对象

集合

总述

Iterable

实现这个接口允许对象成为 “foreach” 语句的目标。 所有单例集合均实现此接口,双利集合Map凭entrySet得到的Set集合是单例的,所以间接的也可以使用foreach。

Collection

抽象类AbstractCollection 实现Collection

抽象类AbstractList 实现List

ArrayListLinkedList,线程不安全

示意图:

MGqXyn.md.jpg

-

MdTT7n.png

-

M0Q461.png

-

ArrayList

ArrayList内部是用Object[]数组实现的,非线程安全,效率高。便于索引,在不扩容的情况下也便于增插。基本来说是增删慢,查询快。

ArrayList扩容机制:若初始集合list没有指定容量,那么默认内部维护的数组长度为0。则首次进行add()操作时,会扩容为DEFAULT_CAPACITY,即为10;若指定了容量,则扩容时,将容量增加一半:newCapacity = oldCapacity + (oldCapacity >> 1)

这个增加有个细节:如果指定的容量为1,那么执行newCapacity = oldCapacity + (oldCapacity >> 1)newCapacity还是1,无法扩容,所以java增加了保护机制,添加判断

if (newCapacity - minCapacity < 0) newCapacity = minCapacity;,此情况适用于指定容量为1时,1-2<0 所以设置newCapacity = 2。保证扩容成功。

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
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//这里保证数组长度为0时,将长度设置为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}
//他这个 minCapacity 变量的意思是指:这次添加操作所需要的最小空间,这个最小空间在常规情况下(数组长度不为0)是size+1;在另一种情况下(数组长度为0),是DEFAULT_CAPACITY,即10
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code 如果最小空间 > 数组长度,就需要扩容了
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//扩容机制:增加一半
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容后,仍然不能满足最小容量,就直接设置新容量为最小容量。(此情况适用于设置的容量为1。保证了原始容量为1时也能扩容成功)
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)//Interger的最大值
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);//将数组elementData拷贝newCapacity个元素返回给elementData
}

ArrayList的插入元素:

1
2
3
4
5
6
7
8
List list = new ArrayList();
list.add(2);
list.add(5);
list.add(1);
list.add(8);

list.add(1,0);
System.out.println(list.toString());

原理:先将index后的元素通过拷贝覆盖的方式后移,然后像普通add()一样elementData[index] = element

1
2
3
4
5
6
7
8
9
10
public void add(int index, E element) {
rangeCheckForAdd(index);

ensureCapacityInternal(size + 1); // Increments modCount!!
//多了这一步,让index后面的元素后移(这里的操作是拷贝覆盖)
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

ArrayList的清空clear():

1
list.clear();//for (int i = 0; i < size; i++) elementData[i] = null;

ArrayList的get():

1
System.out.println(list.get(3));// return (E) elementData[index];

ArrayList的移除元素remove():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
list.remove(2);

public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

其中,重点就是这个System.arraycopy();

1
2
3
4
5
6
7
8
9
/**
* public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
* 代码解释:
Object src : 原数组
int srcPos : 从元数据的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度
*/

举个例子:

1
2
3
4
5
6
7
List list = new ArrayList();
list.add(2);
list.add(5);
list.add(1);
list.add(8);
list.remove(2);
System.out.println(list.toString());

其实就是

1
2
3
4
5
6
int[] arr = {2,5,1,8,0,0,0,0,0,0};//没有设置容量,第一次add()会将length设置为10
int[] brr = {2,5,1,8,0,0,0,0,0,0};
//通过上面一番计算得到数字
System.arraycopy(arr,3,brr,2,7);
System.out.println(Arrays.toString(arr));// {2,5,1,8,0,0,0,0,0,0};
System.out.println(Arrays.toString(brr));// {2,5,8,0,0,0,0,0,0,0};

Vector

底层结构是数组 Object[] ,线程安全(所有操作在方法上面加上synchronized关键字),增删慢,查询快.

如果没有设置初始容量,那么初始容量是10:this(10);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];//这里初始化大小
this.capacityIncrement = capacityIncrement;
}

操作

1
2
3
4
Vector v = new Vector();
v.add(2);
v.addElement(3);//从JDK1.2后开始出现
v.get(0);

LinkedList

LinkedList内部的数据结构是双向链表,因此没有扩容机制这一说。非线程安全,增快,插、查、删都需要遍历index个元素,所以较慢,效率低。

实现原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LinkedList<E> 
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

transient Node<E> first;
transient Node<E> last;

private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}

增加元素add():直接增加,改变指针指向即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//添加元素,保证双向链表为双向的关键操作
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;//l记录之前最后的元素
final Node<E> newNode = new Node<>(l, e, null);//新节点的pre指向之前最后的元素(前指),next指向null
last = newNode;//将维护的last指向最新加入的节点
if (l == null)//如果l为null说明目前只有一个元素
first = newNode;//fist = last = newNode
else//否则将之前最后的元素的next指向新节点(后指)
l.next = newNode;
//通过前指后指完成双向链表
size++;
modCount++;
}

插入元素add(index,e):由于一般情况下都要遍历index次,所以效率较低

1
2
3
4
5
6
7
8
public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
linkBefore(element, node(index));//我们看到这里插入元素时要先执行node(index)遍历index
}

查找元素:get():要遍历index个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

删除元素:remove(index i),remove(Object o):要遍历index个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));//我们看到这里插入元素时要先执行node(index)遍历index
}

public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {//循环遍历
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

比较:

ArrayList在不涉及扩容时,增,查非常快;删,插都要进行System.arraycopy(),通过拷贝覆盖的方式移动元素,所以效率一般,不是太慢。而在涉及扩容时,增操作要计算容量,再进行拷贝操作elementData = Arrays.copyOf(elementData, newCapacity);,所以效率稍微会慢。

LinkedList进行增操作时,只需改变指针指向,较快。而在进行插、查、删操作时都要遍历index个元素才能找到进行操作的位置(然后再改变指针指向),所以较慢。

集合的线程安全性

请问 ArrayList、 HashSet、 HashMap 是线程安全的吗?如果不是我想要线程安全的集合怎么办?

查看这些集合的源码可知,每个方法都没有加锁,显然都是线程不安全的。

在集合中 Vector 和 HashTable 倒是线程安全的。你打开源码会发现其实就是把各自核心方法添加上了
synchronized 关键字 。

如果想要线程安全的集合可以使用Collections工具类:可以让上面那 3 个不安全的集合变为安全的。

1
2
3
4
5
List<Object> list1 = Collections.synchronizedList(new LinkedList<>());
List<Object> list2 = Collections.synchronizedList(new ArrayList<>());
Set<Object> set1 = Collections.synchronizedSet(new HashSet<>());
Set<Object> set2 = Collections.synchronizedSet(new TreeSet<>());
Map map = Collections.synchronizedMap(new HashMap<>());

原理:将集合的核心方法添加上了 synchronized 关键字。

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;

final List<E> list;

SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}

//将list的操作都加synchronized关键字,然后调用其本来的操作。例如
/*public void add(int index, E element) {
synchronized (mutex) {
list.add(index, element);
}
}*/
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return list.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return list.hashCode();}
}

public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}

public int indexOf(Object o) {
synchronized (mutex) {return list.indexOf(o);}
}
public int lastIndexOf(Object o) {
synchronized (mutex) {return list.lastIndexOf(o);}
}

public boolean addAll(int index, Collection<? extends E> c) {
synchronized (mutex) {return list.addAll(index, c);}
}

public ListIterator<E> listIterator() {
return list.listIterator(); // Must be manually synched by user
}

public ListIterator<E> listIterator(int index) {
return list.listIterator(index); // Must be manually synched by user
}

public List<E> subList(int fromIndex, int toIndex) {
synchronized (mutex) {
return new SynchronizedList<>(list.subList(fromIndex, toIndex),
mutex);
}
}

@Override
public void replaceAll(UnaryOperator<E> operator) {
synchronized (mutex) {list.replaceAll(operator);}
}
@Override
public void sort(Comparator<? super E> c) {
synchronized (mutex) {list.sort(c);}
}

private Object readResolve() {
return (list instanceof RandomAccess
? new SynchronizedRandomAccessList<>(list)
: this);
}
}

这里加的锁是mutex,就是this

1
2
3
4
5
6
final Collection<E> c;  // Backing Collection
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}

HashMap

基于数组+链表的数据结构实现,数组的元素是节点Node,节点中有Node next,可形成链表。

非线程安全,高效,支持Null值和Null键,

HashMap究竟是怎么数组加链表实现的:

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
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {

//数组
transient Node<K,V>[] table;

transient Set<Map.Entry<K,V>> entrySet;
transient int size;

//静态内部类
static class Node<K,V> {
final int hash;
//K,V
final K key;
V value;
//链表
Node<K,V> next;

//必须有这些构造方法,否则final没有初始化会报错
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
}

HashMap对元素储存的方式:先调用key的hashCode(),将得到的值对数组长度取模运算,就可以得到此key在数组中对应的位置。然后看该位置是否有元素,如果没有就直接放入;如果有就调用key.equels()对该元素进行比较,看是否返回true,如果是true,代表两元素相同,则使用原key,新value;若返回false,则代表不同,那么就接着链表后面的元素一直比较,直到链表末尾,如果没有相同的,那么就将此键值对添加到链表末尾。

详情请看我的另一篇详细文章:http://hanhanhanxu.coding.me/2019/06/10/Hash/

为什么HashMap的默认初始容量位16?
为了减少hash碰撞,提高了查询速率,使元素较均匀的放在数组中。

解释:计算索引时HashMap是这样计算的:return h & (length-1);

这里的h是key.hashCode(),简单说就是一个数字;length是HashMap的容量,即16。

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
length-1 = 15 //15的二进制为00000000 00000000 00000000 00001111
这时我们考虑,任何一个数字和00000000 00000000 00000000 00001111进行与运算
由于后四位全是1,所以最终每位上得到的数字即可能是0 也可能是1.
例如:2&15
00000000 00000000 00000000 00000010
00000000 00000000 00000000 00001111
得到
00000000 00000000 00000000 00000010 = 2

例如:19&15
00000000 00000000 00000000 00010011
00000000 00000000 00000000 00001111
得到
00000000 00000000 00000000 00000011 = 3

这样可以通过位运算达到取余的结果:2%16 = 2; 19&16 = 3,使最终得到的索引值尽可能均匀分布。



那为什么不是8或者32呢:如果容量不为16,假如是8
length-1 = 7 //7的二进制位00000000 00000000 00000000 00000111
2&7
00000000 00000000 00000000 00000010
00000000 00000000 00000000 00000111
得到
00000000 00000000 00000000 00000010 = 2
15&7
00000000 00000000 00000000 00001111
00000000 00000000 00000000 00000111
得到
00000000 00000000 00000000 00000111 = 7
2%8 = 215%8 = 7,也可以通过位运算达到取余的结果,结果也对啊。那为什么不是8或者32呢??????????

为什么HashMap容量都是2的幂次方

因为得到索引时,可以通过按位与(&)操作来计算余数,比求模(%)更快,而且充分散列,减少碰撞。

当容量是2^n时,h & (length -1) == h % length。

扩容机制:

1
2
static final float DEFAULT_LOAD_FACTOR = 0.75f; //填充因子 
//当hashmap的填充达到75%的时候,会进行扩容resize。条件是:HashMap.Size >= Capacity*LoadFactor

Hashmap的resize()包含扩容ReHash两个步骤,ReHash在并发的情况下可能会形成链表环

  • 扩容
    创建一个新的Entry空数组,长度是原数组的2倍。
  • ReHash
    遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。

为什么HashMap是线程不安全的?

不安全原因:

(1)在put的时候,因为该方法不是同步的,假如有两个线程A,B它们的put的key的hash值相同,不论是从头插入还是从尾插入,假如A获取了插入位置为x,但是还未插入,此时B也计算出待插入位置为x,则不论AB插入的先后顺序肯定有一个会丢失

(2)在扩容的时候,jdk1.8之前是采用头插法,当两个线程同时检测到hashmap需要扩容,在进行同时扩容的时候有可能会造成链表的循环,主要原因就是,采用头插法,新链表与旧链表的顺序是反的,在JDK1.8后采用尾插法就不会出现这种问题,同时1.8的链表长度如果大于8就会转变成红黑树。

为什么ConcurrentHashMap是线程安全的?

ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。

ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。

Hashtable

线程安全(所有操作在方法上面加上synchronized关键字),低效,不支持Null值和Null键

Hashtable的实现:

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
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {

/**
* The hash table data.
*/
private transient Entry<?,?>[] table;//数组

//静态内部类
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
//K,V
final K key;
V value;
//链表 next指针
Entry<K,V> next;

protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
}

LinkedHashMap

HashMap的子类,保存了记录的插入顺序。

Hashmap和Hashtable

  • HashMap不是线程安全的,Hashtable是线程安全的(方法被synchronized修饰)

  • HashMap允许nul的键值。

  • HashMap继承自AbstractMap,Hashtable继承自Dictionary

反射

反射:程序执行期间才知道操作的类是什么,并能在运行期间获得类的完整构造(结构),并可调用方法。

小例子

Apple.java

1
2
3
4
5
6
7
8
9
10
11
12
package Reflex;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Apple {
private Integer price;
}

Demo.java

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
package Reflex;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class Demo {
public static void main(String[] args) {
//正常的调用
Apple apple = new Apple();
apple.setPrice(5);
System.out.println("Apple Price:" + apple.getPrice());
//使用反射调用
try {
//拿到Class对象
Class<?> c = Class.forName("Reflex.Apple");
//拿到对象的构造方法
Constructor<?> con = c.getConstructor();
//通过对象的构造创建实例
Object o = con.newInstance();

//取得set方法
Method set = c.getMethod("setPrice", Integer.class);
//执行set方法。传入参数:实例对象,需set的值
set.invoke(o, 15);
//拿到get方法
Method get = c.getMethod("getPrice");
//执行get方法。
System.out.println("Apple Price:" + get.invoke(o));
}catch (Exception e){
e.printStackTrace();
}
}
}

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
//得到Class对象的3种方法
public static void main1(String[] args)throws ClassNotFoundException {
//1、通过类路径
Class<?> aClass = Class.forName("Reflex.one.User");

//2、通过传入的类
Class<User> aClass1 = User.class;

//3、通过类已经实例化的对象
User user = new User();
Class<? extends User> aClass2 = user.getClass();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//通过Class获得类实例对象的2种方式
public static void main2(String[] args) throws Exception {
Class<User> c = User.class;
//1、通过Class直接newInstance。默认使用无参的构造函数实例化对象
Object instance = c.newInstance();

//2、通过Class先得到类的构造函数,再通过构造函数newInstance。
//好处:可以选择通过哪个构造函数来实例化对象
Constructor<User> constructor = c.getConstructor();
Object user = constructor.newInstance();
//有参的构造函数
Constructor<User> constructor1 = c.getConstructor(Long.class, String.class, String.class);
Object user2 = constructor1.newInstance(111L, "qqq", "eee");

System.out.println(user2);
}
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
//通过Class获取类的属性
public static void main(String[] args) {
Class<User> userClass = User.class;

//getFields()。无法获取私有属性和默认(default修饰)属性
/*Field[] fields = userClass.getFields();
for (Field field : fields) {
System.out.println(field.getName());
}*/

//getDeclaredFields()。获取包括私有,默认属性在内的 全部属性
/*Field[] declaredFields = userClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println(declaredField.getName());
}*/

System.out.println("==========方法==============");
Method[] methods = userClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println("方法名:"+method.getName());
Class<?>[] types = method.getParameterTypes();
for (Class<?> type : types) {
System.out.println(type.getName());
}
}
/*
如果想要获取类的私有属性、方法、构造器,就要用到declared关键字
*/

/*System.out.println("接下来是构造函数:");
Constructor<?>[] declaredConstructors = userClass.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
System.out.println(declaredConstructor.getName());
}*/
}

实战方法

在真正实战中如果面对一个完全未知的类Class,想要通过反射操作此类或对象,那么最常用的是以下方法

1
2
3
4
5
//通常运用反射时,要向方法中传入xxx.class,所以我们已经拿到了Class
Method[] methods = c.getDeclaredMethods();//拿到所有方法
method.getName()//拿到此方法的名称
Class<?>[] types = method.getParameterTypes();//拿到method的参数类型
method.invoke(o,data);//执行对应方法

我们通过拿到的所有方法,找到我们想要的方法,然后获取参数类型,就可执行对应方法。


反射结合POI实战,请移步个人原创:https://github.com/hanhanhanxu/HPoiUtil


多线程

请参见Thread.md

文件IO流

File

File既可以表示文件,也可以表示文件夹

windows的路径分隔符为 \ (因为一个 \ 代表转义字符,所以要使用两个 \ \,第一个 \ 是转义字符,将第二个 \ 转换为它本来的意思),Unix/Liunx下的路径分隔符为 / 。

1
2
3
4
5
6
7
8
9
10
11
12
//利用File创建文件
File file = new File("E:\\0面试\\a.txt");//不存在的文件
boolean newFile = file.createNewFile();
System.out.println(newFile);//true

//当创建已存在的文件时,会返回false
File file = new File("E:\\0面试\\a.txt");
System.out.println(file.createNewFile());//false

//File也可以表示文件夹
File file = new File("E:\\0面试");//已存在的文件夹
System.out.println(file.isDirectory());//true

更专业的方法是使用File.separatorChar,它会根据当前操作系统自动匹配路径分隔符。

1
2
3
4
System.out.println(File.separatorChar);//  \
String filePath = "E:" + File.separatorChar + "0面试";
File file = new File(filePath);
System.out.println(file.isDirectory());//true

通过File遍历某盘/目录下的所有文件:

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
package IO;

import java.io.File;
import java.io.IOException;

public class Demo1 {
private static int num = 0;
public static void main(String[] args) throws IOException {
long beginTimeMillis = System.currentTimeMillis();
File file = new File("F:");
if(file.exists()){
printDir(file);
}
long endTimeMillis = System.currentTimeMillis();
long cost = endTimeMillis - beginTimeMillis;
System.out.println("共有文件:" + num + "个。");
System.out.println("遍历耗时:" + cost + "豪秒。");
}

public static void printDir(File file) {
if(file.isDirectory()){
File[] files = file.listFiles();
for (File file1 : files) {
printDir(file1);
}
}else {
num++;
System.out.println(file.getAbsolutePath());
}
}
}

File可以代表存在或不存在的文件/目录,存在的话可以操作此文件/目录,不存在的话可以创建此文件/目录。

这里的操作只是操作文件的基本信息。如果想要操作文件内部的内容,就需要用到IO流了。

码表

ASCII 美国标准信息交换码。用一个字节的7位表示字符,表示的都是英文字符和数字。

ISO8859-1 拉丁码表,又称Latin-1。欧洲码表,用一个字节的8位表示。ASCII码是包含的仅仅是英文字母,并且没有完全占满256个编码位置,所以它以ASCII为基础,在空置的0xA0-0xFF的范围内,加入192个字母及符号,藉以供使用变音符号的拉丁字母语言使用。从而支持德文,法文等。因而它依然是一个单字节编码,只是比ASCII更全面。

GB2312 英文占一个字节,中文占两个字节。中国的中文编码表。

GBK 中国的中文编码表升级,融合了更多的中文文字符号。

Unicode 国际标准码规范,融合了多种文字,所有文字都用两个字节表示。Java语言使用此码表。

UTF-8 万国码。1~3个字节不等长,英文1字节,中文3字节。最多用三个字节来表示一个字符。

编码:字符串—-》字节数组。String类的getBytes()方法进行编码,将字符串转换为对应的二进制,并且这个方法可以指定编码表,默认使用操作系统的码表。

中国大陆Windows系统默认码表一般为GBK,如果你设置了你的IDE的码表,那么就可能改变。Java程序中可以使用System.getProperty("file.encoding")得到当前默认码表。

解码:字节数组—–》字符串。String类的构造函数完成。可以指定码表。我们使用什么码表编码,就应该使用什么码表解码,否则很有可能出现乱码。

IO

总述图

MQP7LR.md.png

Closeable

1
2
3
4
//Closeable 是可以关闭的数据源或目标。调用 close 方法可释放对象保存的资源(如打开文件)。 
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}

字节流:处理的单元是一个字节。

字符流:字节流+码表。

字节流

抽象父类InputStreamOutputStream

子类FileInputStreamFileOutputStreamBufferedInputStreamBufferedOutputStream

FileInputStream

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
package IO;

import java.io.IOException;
import java.io.InputStream;

/**
* FileInputStream的基本使用
*/
public class FileInputStreamDemo {
public static void main(String[] args) throws IOException {
//如果文件中绝对没有中文的话(只是英文字母和一些*().&*&/*-+这样的纯英文字符),那么可以使用read一个一个字节的读取

//InputStream in = new FileInputStreamDemo("E:\\0面试\\a.txt");
InputStream in = new java.io.FileInputStream("E:\\1MYKNOW\\视频\\java\\java-传智播客基础班\\day23\\准备的数据.txt");


//read() 一个一个字节的读取
//读取得到的返回值是字节对应的ascll码值 a 97,再将int型ascll码值 转换为 char型
/*int sign;
while((sign = in.read()) != -1){
System.out.print((char)sign);
System.out.print(" ");
}*/

//read(byte[]) 字节缓存区读取。字节缓冲区不能全部输出,要根据读取得到的实际长度len进行输出
//将read(byte[]) 一次性读取到的内容放入byte[]中,放入的是数字,即字符对应的ascll码值
/*byte[] bytes = new byte[1024];
int len = in.read(bytes);
for (int i = 0; i < len; i++) {
System.out.print((char)bytes[i]);
}*/

//跳过几个字节后再进行读取
/*in.skip(8);
byte[] bytes = new byte[1024];
int len = in.read(bytes);
for (int i = 0; i < len; i++) {
System.out.print((char)bytes[i]);
}*/

//循环读取整个文件
/*byte[] bytes = new byte[1024];
int len;
while((len = in.read(bytes))!=-1){
for (int i = 0; i < len; i++) {
System.out.print((char)bytes[i]);
}
}*/
in.close();
}
}

FileOutputStream

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
package IO;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
* FileOutputStream的基本使用
*/
public class FileOutputStreamDemo {
public static void main(String[] args) throws IOException {
//如果 E:\0面试 目录下不存在b.txt,则会创建b.txt
/*OutputStream out = new FileOutputStream("E:\\0面试\\b.txt");
out.write('a');*/

//输出流无法创建目录,即 E:\\0面试\\aaa 这个目录不存在,则无法创建,立即报错FileNotFoundException
/*OutputStream out = new FileOutputStream("E:\\0面试\\aaa\\b.txt");
out.write('a');*/

OutputStream out = new FileOutputStream("E:\\0面试\\b.txt");


//不存在的文件b.txt 文件会被创建,且写入ab
/*out.write('a');
out.write('b');*/

//如果文件存在内容,则运行write向文件写入内容,会先将之前的内容全部清空,然后写入cd
/*out.write('c');
out.write('d');*/

//************************写入byte[]

//写入多个字符
/*byte[] bytes = "Hello world.".getBytes();
out.write(bytes);*/
out.close();
}
}

构造器可以传入File,也可以传入String(代表文件路径) BufferedInputStream

1
2
3
4
5
6
7
8
9
10
11
String filePath = "E:\\0面试\\a.txt";
File file = new File(filePath);
FileInputStream in = new FileInputStream(file);



FileInputStream in = new FileInputStream("E:\\0面试\\a.txt");

是一样的。

会抛出FileNotFoundException异常。
字符流

抽象父类ReaderWriter

子类FileReaderFileWriterBufferedReaderBufferedWriter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package IO;

import java.io.IOException;

/**
* 字符流的基本使用
*/

public class Reader {
public static void main(String[] args) throws IOException {
//字符流读取文件
/*FileReader fr = new FileReader("E:\\0面试\\a.txt");
int read;
while((read = fr.read()) != -1){
System.out.print((char)read);
}
fr.close();*/

//字符流写入文件,也会先将原有内容清空,再进行写入。再创建流时选择有参的构造,第二个参数填入true,即可进行追加。
/* FileWriter fw = new FileWriter("E:\\0面试\\b.txt",true);
fw.write("中国,韩旭最牛逼");
fw.close();//close()也会先调用flush(),将缓存刷新出去,不然可以没有真实写出去。*/
}
}
转换流

需要额外传入一个码表

字节流通往字符流:InputStreamReader

字符流通往字节流:OutputStreamWriter

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
package IO;

import java.io.*;

/**
* 转换流 的基本使用
* InputStreamReader
* OutputStreamWriter
*/
public class InReader {
public static void main(String[] args) throws IOException {
//使用InputStreamReader进行读取文件
//inRead();

//使用OutputStreamWriter进行写文件
OutputStream out = new FileOutputStream("E:\\0面试\\a.txt");
OutputStreamWriter ow = new OutputStreamWriter(out,"utf-8");
ow.write("中国还是我韩旭最牛逼!\r\nafdasdf");
ow.close();//必须使用ow.close()才能刷出缓存区,使用in.clost()无法将缓存区数据刷出来。
}

/**
* 发现读取到的数据,中文是乱码,其他正常。则代表我们的原始文件a.txt使用的码表不是GBK。
* 我们去查看a.txt,发现其使用的码表是utf-8,我们将下面码表修改为utf-8,发现读取正常
* @throws IOException
*/
public static void inRead() throws IOException {
InputStream in = new FileInputStream("E:\\0面试\\a.txt");
//按照GBK码表进行读入:读取到的是字节,然后按照码表GBK进行编码为字符
InputStreamReader ir = new InputStreamReader(in,"GBK");
int read;
while((read = ir.read()) != -1){
System.out.print((char)read);
}
in.close();
}
}
拷贝文件
字节流拷贝
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package IO;

import java.io.*;

/**
* 通过FileInOutputStream BufferedInOutputStream 字节流实现文件拷贝
* 操作:
* 开启读取流,读取已存在的文件
* 将读取到的字节
* 开启输出流,输出到新文件中
*/
public class FileCopyByInOut {
public static void main(String[] args) throws IOException {
//精简代码 抛出异常
//InputStream in = new FileInputStream("E:\\0面试\\15-19110H32A9.jpg");
InputStream in = new FileInputStream("E:\\2MYLIFE\\视频\\shuihu\\21.mkv");
OutputStream out = new FileOutputStream("E:\\0面试\\1.mkv");
//OutputStream out = new FileOutputStream("E:\\0面试\\b.jpg");

//73kb的jpg照片

//FileInOutputStream 字节流
//ByteCopy(in, out); //1446毫秒。
//ByteSCopy(in, out); //22毫秒。 6243
ByteSCopyA(in,out); //16521毫秒。

//BufferedInOutputStream 缓冲字节流
//BufferedCopy(in, out); //90ms 24103

out.close();
in.close();

//正确的异常处理方式
/*InputStream in = null;
OutputStream out = null;
try {
//可以拷贝文本、照片、等各种文件
in = new FileInputStream("E:\\0面试\\15-19110H32A9.jpg");
out = new FileOutputStream("E:\\0面试\\b.jpg");

//73kb的jpg照片
//ByteCopy(in, out); //1446毫秒。
//ByteSCopy(in, out); //22毫秒。
}catch (IOException e){
throw new RuntimeException(e);
}finally {
//关闭流
try {
if(out!=null)
out.close();
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
if(in!=null)
in.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}*/
}

public static void BufferedCopy(InputStream in, OutputStream out) throws IOException {
long begin = System.currentTimeMillis();
BufferedInputStream bin = new BufferedInputStream(in);
BufferedOutputStream bout = new BufferedOutputStream(out);

int len;
while((len = bin.read())!=-1){
bout.write(len);
}
long end = System.currentTimeMillis();
long cost = end - begin;
System.out.println("本次操作耗时:"+ cost + "毫秒。");
}

/**
* 比下面那个更好的一点是拷贝前后文件完全一致,不会出现多余内容
* 拷贝文件就用这个********************************************************可以拷贝任何文件数据100%成功********************************
* @param in
* @param out
* @throws IOException
*/
public static void ByteSCopyA(InputStream in, OutputStream out) throws IOException {
long begin = System.currentTimeMillis();
int len;
byte[] bytes = new byte[1024];
//使用缓冲区进行拷贝,当最后一次循环进行时,bytes可能未被装满,但是由于bytes.length是1024,所以仍会将1024大小的空间填充到out里(多余的是0),就出现了大小不一致
while((len = in.read(bytes)) != -1){
out.write(bytes,0,len);
}
long end = System.currentTimeMillis();
long cost = end - begin;
System.out.println("本次操作耗时:"+ cost + "毫秒。");
}

/**
* 优点:可以拷贝任何内容,快速
* 缺点:仔细观察发现拷贝前后两文件大小不一致 72.6kb -> 73kb
* @param in
* @param out
* @throws IOException
*/
public static void ByteSCopy(InputStream in, OutputStream out) throws IOException {
long begin = System.currentTimeMillis();
int len;
byte[] bytes = new byte[1024];
//使用缓冲区进行拷贝,当最后一次循环进行时,bytes可能未被装满,但是由于bytes.length是1024,所以仍会将1024大小的空间填充到out里(多余的是0),就出现了大小不一致
while((len = in.read(bytes)) != -1){
out.write(bytes);
}
long end = System.currentTimeMillis();
long cost = end - begin;
System.out.println("本次操作耗时:"+ cost + "毫秒。");
}

/**
* 优点:可以拷贝任何内容
* 缺点:拷贝很慢,因为是一个字节为单位进行拷贝的
* @param in
* @param out
* @throws IOException
*/
public static void ByteCopy(InputStream in, OutputStream out) throws IOException {
long begin = System.currentTimeMillis();
int read;
//一个字节为单位的进行拷贝,所以无论是什么文件内容都可以被正确拷贝。文件中可以存在中文。
while((read = in.read())!= -1){
out.write(read);
}
long end = System.currentTimeMillis();
long cost = end - begin;
System.out.println("本次操作耗时:"+ cost + "毫秒。");
}
}

/*
输入输出流
一般先打开的后关闭
后打开的先关闭。


特殊情况下也要看两个流的依赖情况。
*/
字符流拷贝
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
package IO;

import java.io.*;

/**
* 通过字符流拷贝文件
* 字符流只能拷贝文字文件,不能拷贝图片,视频音频等。
* 因为这些文件都是以二进制进行存放,字符流读取到之后对他们进行编码处理,但是并不是所有读取到的二进制都能其码表中找到对应的字符,所以就可能造成数据丢失。
*/

public class FileCopyByRW {
public static void main(String[] args) throws IOException {

System.out.println(System.getProperty("file.encoding"));

//"E:\\0面试\\a.txt"
//"E:\\0面试\\b.txt"

//一个一个字符的拷贝文件
/*FileReader fr = new FileReader("E:\\0面试\\a.txt");
FileWriter fw = new FileWriter("E:\\0面试\\b.txt");

int len;
while((len = fr.read()) != -1){
fw.write(len);
}

fw.close();
fr.close();*/

//使用缓冲数组来拷贝文件
/*FileReader fr = new FileReader("E:\\0面试\\a.txt");
FileWriter fw = new FileWriter("E:\\0面试\\b.txt");

int len;
char[] arr = new char[1024];
while((len = fr.read(arr)) != -1){
fw.write(arr,0,len);
}

fw.close();
fr.close();*/

}
}

IO总结

何时使用字符流,何时使用字节流?依据是什么?

使用字符流的应用场景: 如果是读写字符数据的时候则使用字符流。

使用字节流的应用场景: 如果读写的数据都不需要转换成字符的时候,则使用字节流。

字节流

输入字节流: ———| InputStream 所有输入字节流的基类。

抽象类 ————| FileInputStream 读取文件的输入字节流

​ ————| BufferedInputStream 缓冲输入字节流,其实该类内部只不过是维护了8kb的字节数组而已。 出现的目的主要是为了提高读取文件的效率。

输出字节流: ———| OutputStream 所有输出字节流的基类。

抽象类 ————–| FileOutputStream 向文件输出数据的输出字节流。

​ ————–| BufferedOutputStream 向文件输出数据的输出字节流。

字符流

输入字符流: ———-| Reader 所有输入字符流的基类。

抽象类 ————–| FileReader 读取文件字符的输入字符流 。

​ ————–| BufferedReader 缓冲输入字符流, 该类出现的目的主要是为了提高读取文件的效率与拓展功能(readLine)。

输出字符流 ———| Writer 所有输出字符流的基类。

抽象类。 ————-| FileWriter 向文件输出数据的输出字符流。

​ ————-| BufferedWriter 缓冲输出字符流, 该类出现 的目的是为了提高写文件数据的效率与拓展功能。

转换流

输入字节流的转换流

InputStreamReader InputStream——————–> Reader

输出字节流的转换流

OutputStream OutputStream ——————–> Writer

转换流的作用:

  1. 可以把字节流转换成字符流使用。
  2. FileReader与FileWriter都是固定是gbk码表进行读写数据的,而转换流可以指定码表进行读写文件的数据。 Properties(配置文件类) 体系: ——-| Map ————| HashTable —————-| Properties 配置文件类、

store() 用于生成一个配置文件 load() 加载一个配置i文件 注意:

  1. 如果配置文件存在着中文,那么生成配置文件的时候要使用字符流,否则会出现乱码。
  2. 如果需要修改配置文件的内容,应该先加载原本配置文件,然后再生成一个配置文件。

class文件怎么读

除了javac还有哪些常用的工具

insist,on the road
-------------本文结束感谢您的阅读-------------