玩命加载中 . . .

二刷Java基础


二刷Java基础

java概述

概念

JDK(开发工具包):是提供开发人员是hi用的,java开发工具、包括JRE

JRE(运行环境):包括java虚拟机和所需要的核心类库,可以运行java程序

JVM:虚拟机

关键字:被java赋予了特殊的含义,所有字母都是小写

保留字:java尚未使用,但之后可能会作为关键字使用,命名时避开

标识符:给变量、方法、类等元素命名的字符

Java特点

  1. 纯粹的面向对象
    1. 类,对象
    2. 封装、集成、多态
  2. 健壮性
    1. 去掉了c中的指针
    2. 增加了垃圾回收机制
  3. 跨平台性
    1. JVM虚拟机
    2. Java代码–字节码文件–JVM虚拟机–操作系统–硬件

标识符

命名规则

  1. 26英文字符、0-9,_或$
  2. 数字不能开头
  3. 不能使用关键字和保留字
  4. 严格区分大小写,长度无限制
  5. 不能有空格

命名规范

  1. 包名:多单词组成全小写

  2. 类、接口名:多单词首字母大写

  3. 变量、方法名:第一个首字母小写,其他驼峰命名

  4. 常量名:全大写,多单词用下划线连接

变量

数据类型

整数类型(byte,short,int,long)

浮点型(float,double)

E表示10的多少次方,如E38就是10的38次方

字符类型(char)

2字节,用单引号括起来

  1. 声明一个字符
  2. 转义字符'\n'
  3. Unicode值表示值
String引用数据类型

可以与8中基本数据类型做运算,运算只能是+

3+4+"hello" ---->  7hello
"hello"+3+4 ---->  hello34

String a = 4;//错
String a = 4+"";//对

类型转换

自动类型转换

(char、byte、short)–int–long–float–double

short s = 5;
s = s-2;//编译不通过,因为2是int,所以结果是Int
s = (short)(s-2);//通过,强制类型转换
强制类型转换

强转符()

如:double d1 = 12.9; int i2=12

进制转换

二进制转十进制

10101,从右往左:2^0^x1+2^1^x0+2^2^x1+2^3^x0+2^4^x1=21

十进制转二进制

21,除2取余的逆:商10余1,商5余0,商2余1,商1余0,商0余1

取逆:10101

二进制转八进制、十六进制

八进制:3个一位

十六进制:4个一位

原码、反码、补码:

基本语法

算术运算符

取余运算%

结果的符号与被模数符号相同

++,–

比较运算符

instanceof

检查是否是类的对象

如:Hello instanceof String 结果:true

逻辑运算符

&&,|| 短路与/或
^异或

判断两个是否不同,不同为true,同为false

位运算符

<<:左移,如:int i = 21; i<<2,结果:i=84,过程:21*2^2^

三元运算符

格式:(条件表达式)?表达式1:表达式2

如果条件为true,结果为表达式1,否则为2

流程控制

1.switch case

表达式中只能有6种数据类型:

byte、short、char、int、枚举、String

switch(表达式){
      case 常量1:
          语句;
          break;//不加的话,会按顺下往下执行
      case 常量2:
          语句;
          break;
    default://最终兜底,可写可不写
        语句;
        break;
}

2.for

for(初始化条件;循环条件(boolean类型);迭代条件){
    循环体
}
for(int i =1;i<=10;i++){
    System.out.println("hello");
}

3.while(先判断后执行)

while(循环条件){
    循环体
    迭代条件
}
int i=1;
while(i<=10){
    System.out.println("hello");
    i++;
}

while(true)无限循环:使用break来跳出

4.do-while(先执行后判断)

do{
  循环体
  迭代条件
}while(循环条件)

数组

概述

  1. 是引用数据类型,元素可以是任何数据类型
  2. 在内存种开辟一整块内存空间
  3. 数组长度一旦确定,不能修改
  4. 角标从0开始

一维数组

  1. 初始化

    必须指定长度,内存才会分配空间

    //静态初始化
    int[] array1;
    array1 = new int[]{1,2,3};
    int arr3[] = {1,2,3};
    //动态初始化
    String[] array2 = new String[5];
    //默认初始化值:
    整型:0,浮点型:0.0,char:0,boolean:false,引用数据类型:null
    
  2. 调用

    array1[0]="小明";
    //获取长度
    array1.length
    //遍历,条件表达式没有等号
    for(int i=0;i<array1.length;i++){
        System.out.println(array[i]);
    }
    
  3. 内存解析

二维数组

  1. 初始化

    int[][] arr1 = new int[][]{{1,2,3},{1,2}};
    int[][] arr2 = new int[3][2];
    int[][] arr3 = new int[3][];
    int[][] arr4 ={{1,2},{1,2,3}};
    
  2. 调用

    arr1[0][1]
    //获取长度
    String[][] arr4 = {{1,2,3},{1,2,3,4},{1,1,1}}
    arr4.length//3
    arr4[0].length//3
    //遍历
    for(int i= 0;i<arr4.length;i++){
        for(int j = 0;j<arr4[i].length;j++){
            System.out.println(arr4[i][j]);
        }
    }
    
  3. 默认初始化值

    int[][] arr = new int[4][3]

    外层元素:arr[0],地址值

    内层元素:arr[0] [0],0

    int[][] arr = new int[4][]

    外层元素:地址值

    内层元素:空指针,因为没有指定内存空间

  4. 内存解析

  5. 二维数组算法

    /**
    *杨辉三角
    *要求:1.打印一个10行杨辉三角
    2.第一行有1个元素,第n行有n个元素
    3.每一行的第一个元素和最后一个元素是1
    4.从第三行开始,对于非第一个元素和最后一个元素,
    yanghui[i][j]=yanghui[i-1][j-1]+yanghui[i-1][j]
    **/
    public void test(){
        //声明并初始化数组
        int[][] yangHui = new int[10][];
        //给数组赋值
        for(int i = 0;i<yangHui.length;i++){
            yangHui[i] = new int[i+1];
            //给首位元素赋值
            yangHui[i][0]=yangHui[i][i]=1;
            //给中间赋值,这个判断语句可以省略,因为当i为0或1时,for判断语句也进不去
           // if(i>1){
                //从第二个数组开始,到倒数第二个数组结束
                for(int j = 1;j<yangHui[i].length-1;j++){
                    yangHui[i][j]=yangHui[i-1][j-1]+yangHui[i-1][j]
                }
            //}
        }
        //遍历数组
        for(int i = 0;i<yangHui.length;i++){
            for(int j = 0;j<yangHui[i].length;j++){
                System.out.println(yangHui[i][j]);
            }
        }
    }
    
    多维数组的使用
        声明:int[] x,y[],以下允许通过编译的是
        //首先,int[] x,是一个一维数组,y是一个二维数组
        x[0] = y;//不可以,int=二维数组
        y[0] = x;//可以,一维=一维
        y[0][0] = x;//不可以,int = 一维
        y[0][0] = x[0];//可以,int = int
    
    定义一个int型一维数组,包含10个元素,分别赋予随机整数,求所有元素的最大值最小值,和,平均值
    要求:所有随机数都是两位数
        公式:[a,b]的整数,(int)(Math.random()*(b-a+1)+a)
        //(int)(Math.random()*(99-10+1)+10)
        
    public void test(){
        int[] arr = new int[10];
        for(int i=0;i<arr.length;i++){
            arr[i]=(int)(Math.random()*(99-10+1)+10)
        }
        //最大值
        int max = arr[0];
        for(int i=1;i<arr.length;i++){
            if(max<arr[i]){
                max=arr[i];
            }
        }
        //最小值
        int min = arr[0];
        for(int i=1;i<arr.length;i++){
            if(min>arr[i]){
                min=arr[i];
            }
        }
        //和
        int sum = 0;
        for(int i=0;i<arr.length;i++){
            sum+=arr[i];
        }
        //平均值
        int avg = sum/arr.length;
    }
    
    //数组复制
    int[] arr1 = {1,2,3};
    int[] arr2 = new int[arr1.length];
    for(int i = 0;i<arr1.length;i++){
        arr2[i]=arr1[i];
    }
    
    //数组反转
    int[] arr1 = {1,2,3};
    for(int i = 0;i<arr1.length / 2;i++){
        int temp = arr1[i];
        arr1[i]=arr1[arr1.length-i-1];
        arr1[arr1.length-i-1] = temp;
    }
    

数组查找

线性查找
public void test(){
    String[] arr1 ={"a","b","c"}
    String dest = "b";
    boolean isFlag = true;
    for(int i=0;i<arr1.length;i++){
        if(dest.equals(arr1[i])){
            System.out.println("找到了");
            isFlag = false;
            break;
        }
    }
    if(isFlag){
        System.out.println("没有找到")
    }
}
二分法查找
//前提:要查找的数组必须是有序的
public void test(){
    int[] arr1 = {-20,-10,-5,0,11,18,23,55};
    int dest = -10;//要查找的数
    boolean isFlag = true;
    int head = 0;//起始索引
    int end = arr1.length-1;//末索引
    while(head<=end){
        int middle = (head+end)/2;//中间索引
        if(dest==arr1[middle]){
            System.out.println("找到了,位置在"+middle);
            ifFlag = false;
            break;
        }else if(dest>arr1[middle]){
            head = middle+1;
        }else{//dest<arr1[middle]
            end = middle-1;
        }
    }
    if(isFlag){
        System.out.println("没有找到");
    }
}

排序算法

算法的5大特征
  1. 有输入
  2. 有输出
  3. 有穷性:能结束,不是死循环
  4. 确定性:有确定含义,没有二义性
  5. 可行性:清楚可行

选择排序:堆排序

交换排序:==冒泡排序(n^2^)、快速排序(nlog2n)==,平均时间上:快速排序最佳

归并排序

//冒泡排序
public void test(){
    int[] arr1 = {1,2,-20,-10,12,5,3,15};
    
    for(int i = 0;i<arr1.length-1;i++){//负责控制几轮,8个元素,i<7,共7轮,第8轮就自己不需要排序
        for(int j = 0;j<arr1.length-1-i;j++){//负责控制对比的次数,第一轮对比7次,第二轮对比6次
            int temp = arr1[j];
            arr1[j] = arr1[j+1];
            arr1[j+1] = temp;
        }
    }
}
//快速排序
public static void quickSort(int[] arr,int left,int right){
    int i =left;//先备份,保证ij变动,传入的参数不变
    int j = right;
    int key = arr[left];
    int temp ;
    if(i>j){
        return;
    }
    if(arr.length=null||arr.length=0){
        return;
    }
    while(i<j){
        while(i<j&&arr[right]>=key){
            j--;
        }
        while(i<j&&arr[left]<=key){
            i++;
        }
        while(i<j){
            t = arr[i];
            arr[i] = arr[j];
            arr[j] = t;
        }
    }
    arr[left] = arr[i];
    arr[i] = key;
    quickSort(arr,left,j-1)quickSort(arr,j+1,right);
}

Arrays工具类

1 boolean equals(int[] a,int[] b) 判断两个数组是否相等
2 String toString(int[] a) 输出数组信息
3 void fill(int[] a,int val) 将只等值填充到数组之中
4 void sort(int[] a) 对数组进行排序
5 int binarySearch(int[] a,int key) 对排序后的数组进行二分法检索指定的值

数组常见异常

  1. 数组角标越界异常:ArrayIndexOutOfBoundsException
  2. 空指针异常:NullPointerException

面向对象

对象的内存分析

  1. 堆:存放对象实例(包括对象中的属性(成员变量))
  2. 栈:存放局部变量(保存在方法种的变量)
  3. 方法区:存放类信息、常量、静态变量

变量

成员变量与局部变量:

  1. 成员变量:类中的属性,加载到堆空间
  2. 局部变量:方法中的属性,加载到栈空间
  3. 成员变量有初始化值
  4. 局部变量没有初始化值,引用时需要显式赋值

方法

  1. 格式:权限修饰符 返回值类型 方法名(形参列表){方法体}

  2. 关键字[可选]:static、final、abstract

  3. 权限修饰符:private、缺省、protected、public(类内部,同一个包,不同包的子类,同一个工程)

重载

两同一不同:

同一个类,相同方法名,

参数列表不同:参数个数不同,参数类型不同

可变个数形参

允许直接定义能和多个实参类型相匹配的形参

public void show(String ... strs)

构造器

  1. 如果没有显式定义类的构造器的话,系统默认提供一个空参的构造器
  2. 一旦显示定义了类的构造器之后,系统就不会再提供空参的构造器
  3. 一个类至少会有一个构造器

关键字

this

在类中,this指的是这个类的对象,通过this.属性来调用对象的成员变量。一般情况下属性和形参重名才会使用。

也可以调用构造器,如this(),就是调用的空参,也可以写有参的this(id,age),一般都放在方法的首行

super

  1. 在子类的方法或构造器中,通过super.属性或super.方法,显式调用父类中声明的属性或方法
  2. 当子类和父类定义了同名的属性/方法,我们需要用super.属性表示调用的是父类中声明的属性

static

  1. 静态的
  2. static可以用来修饰:属性(类变量)、方法、代码块、内部类
  3. 静态变量(类变量):
    1. 随着类加载而加载,早于对象的创建。
    2. 可以通过“类.静态变量”来调用
    3. 类不能调用实例变量Student.name是不行的,如果是类变量可以
  4. 静态方法
    1. 随着类加载而加载,可以通过“类.静态方法”的方式进行调用
    2. 类不能调用非静态方法或属性
    3. 非静态方法可以调用静态方法和属性,也可以调用非静态
    4. 在静态方法内,不能使用this关键字,super关键字
    5. 什么时候声明static:属性被多个对象共享的,不会随对象而不同。操作静态属性的方法。工具类

final

  1. 可以用来修饰的结构:类、方法、变量

  2. 类:此类不能被继承

  3. 方法:此类不能被重写

  4. 变量:让其变为一个常量,不能再修改

    1. 修饰属性:显示初始化、代码块中初始化、构造器中初始化(用于值不一样的时候)

    2. 修饰局部变量:修饰形参时,表明形参是一个常量,当我们调用此方法,给常量形参赋予一个实参,以后调用以后就只能再方法体内使用该形参,不能进行重新赋值

代码块

  1. 作用:用于初始化类、对象
  2. 代码块只能用static修饰
  3. 静态代码块:
    1. 内部可以有输出语句
    2. 随着类的加载而执行,且只执行一次
    3. 类中可以定义多个静态代码块
    4. 静态代码块执行优先于非静态代码块
    5. 静态代码块内只能调用静态属性,静态方法,不能调用非静态结构
  4. 非静态代码块
    1. 内部可以有输出语句
    2. 随着对象的创建而执行
    3. 每创建一次就会被执行一次
    4. 作用:可以在创建对象时,对对象的属性进行初始化

封装

属性私有,get,set

继承

  1. 只支持单继承和多层继承
    1. 1个子类只能有1个父类
    2. 1个父类可以有多个子类

多态

  1. 父类的引用指向子类的对象Person p1 = new Man();

  2. 虚拟方法调用:当调用方法时,编译期,只能调用父类声明的方法。但在运行期,我们实际执行的时子类重写父类的方法。

    总结:编译,看左边;运行,看右边(运行时行为

  3. 作用:在方法上的形参使用父类,当调用方法时传入子类对象,就会是子类调用方法。

  4. 注意:对象的多态性只适用于方法,不适用于属性

  5. 向下转型:父类想转型为子类,需要加上强制转换(子类),但如果已经多态使用了向上转型,再转其他类型就会报错,如:Person p2 = new Man(); Woman woman =(Woman)p2;就会报错ClassCastException

  6. instanceof:为了避免在向下转型时出现异常,我们在向下转型之前,先进行instanceof的判断,一旦为true,就进行向下转型,flase就不转型。

重写

  1. 子类不能重写父类声明为private权限的方法

抽象类

public abstract class test{}

  1. 抽象类是继承重写

  2. 抽象类不能实例化

  3. abstract不能用来修饰:属性、构造器

  4. 不能用来修饰私有方法、静态方法、final方法

模板方法设计模式

  1. 在软件开发实现一个算法时,整体步骤很固定通用,这些步骤已经在父类写好了。某些部分易变,易变的部分可以抽象出来,给子类实现。

接口

  1. 先写extends,后写implements

  2. 和类是相同级别,如果父类和接口定义了相同变量(非方法,方法是谁重写用谁的,都没重写类优先),需要加前缀,super/接口名.变量

  3. 如果同时实现两个接口中的同名方法,则都重写,调用时接口名.super.方法,没有重写就报错。

  4. 接口中的对象也是final的不能修改。

  5. jdk7及之前:只能定义全局常量和抽象方法

    1. 全局常量:public static final ,书写时可以省略
    2. 抽象方法:public abstract
  6. jdk8:除了定义全局常量和抽象方法意外,还可以定义静态方法、默认方法

    1. 静态方法只能通过接口调用
    2. 通过实现类的对象,可以调用接口中默认方法
    3. 如果实现类重写了接口中的默认方法,仍然调用的是重写后的方法(类优先原则)
  7. 接口中不能定义构造器,意味着接口不可以实例化

  8. 接口与接口间是继承,而且可以多继承

  9. 抽象类和接口有哪些异同

    1.都不能实例化
    2.实现的方式都体现了多态
    
  10. 接口的应用:代理模式

    创建代理对象,构造器中传入被代理对象,调用代理对象的方法,则会执行被代理对象方法和代理对象自带的方法

内部类

分类:成员内部类(静态、非静态) vs 局部内部类(方法内、代码块内、构造器内)

  1. 成员内部类:
    1. 作为外部类的成员
      1. 调用外部类的结构
      2. 可以被static修饰
      3. 可以被4种不同的权限修饰
    2. 作为一个类
      1. 类内可以定义属性方法构造器
      2. 可以被final修饰,表示此类不可被继承
      3. 可被abstract修饰
  2. 创建静态成员内部类Person.Dog dog = new Person.Dog();
  3. 创建非静态成员内部类Person p = new Person(); Person.Bird bird = p.new Bird();
  4. 调用属性:
    1. 内部类属性:this.name
    2. 形参:name
    3. 外部类变量:外部类名.this.name
    4. 属性名不同可直接调用

常用类的使用

Object类的使用

  1. final、finally、finalize的区别

    final是关键字,标记在属性上,该属性就不会被改变;标记在方法上,该方法不会被重写,标记在类上,该类不会被继承
    finally是在异常中,最后一定执行的语句块
    finalize是垃圾回收的方法
    
  2. == 和 equals()的区别

    ==运算符
    如果比较的是基本数据类型:比较两个变量保存数据是否相等
    如果比较的是引用数据类型:比较两个变量的地址值是否相等,即引用的是不是同一个实体
    
    equals()方法
    只适用于引用数据类型:Object类中的equals方法:和==的作用是相同的,一样是比较地址值
    其他类可以重写equals方法,来进行比较两个对象的实体内容是否相同
    
    //重写equals方法
    public boolean equals(Object obj){
        if(this == obj){//如果两个都指向一个对象,肯定相等
            return true;
        }
        if(obj instanceof Person){//如果是一个类型,进行比较,不是直接返回false
            Person p1 = (Person) obj;
            return this.age == p1.age && this.name.equals(p1.age);//这里name是字符串,需要使用字符串重写的equals方法,否则还是比较的地址
        }else{
            return false;
        }
    }
    

    注意:使用equals方法时,最好先判断x.equals(y)的x是不是Null,如果是null会报空指针异常

  3. toString()

    在没有重写时,obj类中输出的是地址值,name@哈希值
    重写之后,返回的是实体内容信息
    

单元测试方法的使用

  1. 此类中是public,此类提供公共的无参构造器
  2. 方法上@Test

包装类的使用

  1. java提供8种基本数据类型对应的包装类,使得基本数据类型变量具有类的特征

  2. 基本数据类型—>包装类:new Integer()

  3. 包装类—>基本数据类型:.xxxvalue()

  4. 自动装箱:int num1 = 1; Integer a1 = num1;,不需要new了

  5. 自动拆箱:Integer b1 = new Integer(); int num = b1;,不需要xxxvalue()了

  6. 基本数据类型/包装类 —>String类型

    1. 方式1:连接运算:String str1 = num1+"";

    2. 方式2:调用String的valueOf(xxx)的方法:String str2 = String.valueOf(num2);

    3. 方式3:调用包装类的parseXXX()方法,Integer.paseInt(str1);

      注意:这里如果转换不正确,会出现NumberFormatException,检查数据是否正确

main()方法的使用

  1. main()方法作为程序的入口
  2. main()方法也是一个普通的静态方法
  3. main()方法可以作为我们与控制台交互的方式 run configurations–arguments–program arguments–输入参数

String类

  1. 是一个final类,代表不可变的字符序列
    1. 当对字符串重新赋值时,会在内存区重新开辟一个空间进行赋值
    2. 当对现有的字符串进行连接操作时,也会在内存区重新开辟一个空间进行赋值
    3. 当调用String的repalce()方法修改指定内存或字符串时,也会在内存区重新开辟一个空间进行赋值
  2. 底层是数组
  3. String实现了Serializable接口:表示字符串是支持序列化的
  4. String实现了Comparable接口:表示String可以比较大小
  5. 每个字面量在内存中只会存在一个,如果a = "123",b = "123",则这两个指向的是同一个地址

String对象的创建

  1. String s1 = "123"
  2. String s2 = new String("123")

这两种情况有什么区别?

  1. 第一种情况,s1的数据声明在方法区的常量池中
  2. 第二种情况,s2保存的是地址值,数据在堆空间中开辟空间以后对应的地址值
  3. 两者不相等
  4. 第二种情况在内存中有两个对象:1个是堆空间中new结构,另外一个是char[]对应的常量池中的数据”123”

String对象之间的对比

String s1 = "hello";
String s2 = "world";
String s3 = "hello" + "world";
String s4 = "helloworld";
String s5 =  s1 + s2;
String s6 = s1 + "world";
String final s7 = s1 + "world";//因为final之后是常量,在内存中是在常量池的位置

s3 == s4;//true
s3 == s5;//false
s3 == s6;//false
s3 == s7;//true

结论:

  • 常量与常量之间的拼接,结果仍然在常量池,常量池中不会存在任何相同内容的常量
  • 只要其中有一个是变量,结果都是在堆中
  • 如果拼接的结果调用intern()方法,返回值就是在常量池中

String对象的值传递

public class StringTest{
    String str = new String("good");
    char[] ch = {'t'.'e'};
    public void change(String str,char ch[]){//因为这里的形参是另一个对象,它指向了另一个字面量,并不会影响到原对象
        str="test";
        ch[0] = 'b';//形参是另一个对象,两个指向的都是同一个数组,改变数组就改变原值
    }
    main(){
        StringTest ex = new StringTest();
        ex.change(ex.str,ex.ch);
        ex.str;//good
        ex.ch;//be
    }
}

String常用方法

int length() 返回字符串长度
char charAt(int index) 返回某索引处的字符(从0开始)
boolean isEmpty() 判断是否为空字符串
String toLowerCase() 将所有字符转换为小写(不会影响原字符串)
String toUpperCase() 将所有字符转换为大写
String trim() 取出首位空白
boolean equals(Object obj) 比较字符串内容是否相同
boolean equalsIgnoreCase(String anotherString) 忽略大小写比较内容
int copareTo(String anotherString) 比较两个字符串大小
String substring(int beginIndex) 将字符串从开始索引截取到最后,从0开始
String substring(int beginIndex,int endIndex) 左闭右开区间[)截取,从0开始
前后缀
boolean endsWith(String suffix) 测试此字符串是否以指定后缀结束
boolean startsWith(String prefix) 测试此字符串是否以指定前缀开始
boolean startsWith(String prefix,int toffset) 测试此字符串从指定索引开始有指定字符串
查找
boolean contains(String str) 判断是否包含字符串
int indexOf(String str) 返回指定字符串在此字符串第一次出现的索引
int indexOf(String str,int fromIndex) 返回指定字符串在此字符串第一次出现的索引,从索引处开始查找
int lastIndexOf(String str) 从右边返回指定字符串在此字符串第一次出现的索引
int lastIndexOf(String str,int fromIndex) 从右边返回指定字符串在此字符串第一次出现的索引,从索引处开始查找
注意 indexOf和lastIndexOf没有找到返回的都是-1
替换
String replace(char old,char new) 返回新字符串,将新字符替换旧字符
String replace(String old,String new) 返回新字符串,将新字符串替换旧字符串
String replaceAll(regex正则,String new) 返回新字符串,将新字符串替换全部的匹配正则的字符串
String replaceFirst(regex正则,String new) 返回新字符串,将新字符串替换第一个的匹配正则的字符串
匹配
boolean matches(String regex) 告知此字符串是否匹配给定的正则表达式
切片
String[] split(String regex) 根据给定正则表达式的匹配拆分此字符串
String[] split(String regex,int limit) 根据匹配给定的正则表达式来拆分此字符串,最多不超过限制的个数

String的类型转换

与基本类型转换

基本数据类型/包装类 —>String类型

  1. 方式1:连接运算:String str1 = num1+"";
  2. 方式2:调用String的calueOf(xxx)的方法:String str2 = String.valueOf(num2);

String类型 —>基本数据类型/包装类

  1. 调用包装类的parseXXX()方法,Integer.paseInt(str1);
与char类型转换

String —> char[]

  1. 调用String的toCharArray()

char[] —> String

  1. 调用String的构造器
与byte类型转换

String —> byte[]

  1. 调用String的getBytes[]
  2. 也可以指定编码的字符集:getBytes[指定字符集”gbk”]

char[] —> String

  1. 调用String的构造器(默认使用默认的解码)
  2. 出现乱码是因为编码和解码不一致

StringBuffer类

String、StringBuffer、StringBuilder三者的异同

String:不可变的字符序列;底层使用char[]数组进行保存

StringBuffer:可变的字符序列:线程安全的,效率低;底层使用char[]数组进行保存

StringBuilder:可变的字符序列:线程不安全的,效率高;底层使用char[]数组进行保存

效率对比:StringBuilder > StringBuffer > String

源码分析:

问题1:System.out.println(sb.length())长度为0

原因1:虽然StringBuffer在底层为我们创建了一个长度为16的char数组,但在没有给StringBuffer赋值时,输出的长度自然是0,是按赋值的长度输出的

问题2:扩容问题,如果在添加数据到底层数组装不下,即超过16时,底层为我们自动扩容。

原因2:默认情况下,扩容为原来容量的2倍+2,同时将数组的元素复制到新数组中

指导意见:开发时最好使用StringBuffer(int capacity)或StringBuilder(int capacity)。参数就是我们指定的长度,避免经常扩容

小问题:

public void test(){
    String str = null;
    StringBuffer sb = new StringBuffer();
    sb.append(str);//底层调用的是拼接字符串
    sb.length()//4
    sb//"null"
        
    StringBuffer sb1 = new StringBuffer(str);//抛异常,NullPointerException。底层调用的是str.length(),空指针
}

StringBuffer常用的方法

StringBuffer append(xxx) 增:提供了很多append方法,用于进行字符串拼接
StringBuffer delete(int start,int end) 删:删除指定位置的内容
StringBuffer replace(int start,int end,String str) 改:把[start,end)位置替换为tr
StringBuffer insert(int offset,xxx) 插:在指定位置插入xxx
StringBuffer reverse() 把当前字符序列逆转
int indexOf(String str) 返回指定字符串在此字符串第一次出现的索引
String substring(int start,int end) 将字符串从开始索引截取到结束,从0开始
int length() 长度:返回长度
char charAt(int n) 查:返回某索引处的字符(从0开始)
void setCharAt(int n,char ch) 设置某索引处的字符

遍历:for(sb.length())+charAt() / toString()

String算法题

//将字符串中指定位置进行反转
public String reverse(String str,int startIndex,int endIndex){
    if(str != null){
        //第一部分
        String reverseStr = str.substring(0,startIndex);
        //第二部分
        for(int i = endIndex;i >= startIndex;i--){
            reverseStr += str.charAt(i);
        }
        //第三部分
        reverseStr += str.substring(endIndex + 1);
        return reverseStr;
    }
    return null;
}
//方式2:使用StringBuffer/StringBuilder替换String
public String reverse2 (String str,int startIndex,int endIndex){
    if(str != null){
        StringBuffer sb = new StringBuffer(str.length());
        //第1部分
        sb.append(str.substring(0,startIndex));
        //第2部分
        for(int i = endIndex;i>=startIndex;i--){
            sb.append(str.charAt(i));
        }
        //第3部分
        sb.append(str.substring(endIndex+1));
        return sb.toString();
    }
    return null;
}
//获取一个字符串在另一个字符串中出现的次数
public int getCount(String mainStr,String subStr){
    int count = 0;
    int mainLength = mainStr.length();
    int subLength = subStr.length();
    int index = 0;
    if(mainLength >= subLength){
        while((index = mainStr.indexOf(subStr,index))!=-1){//从index处开始查找,返回索引,第一次为0,之后依次从索引+sublength处开始继续查找
            count ++ ;
            index += subLength;
        }
    }else{
        return 0;
    }
}

时间类

时间戳

System.currentTimeMillis():1970年1月1日0点到现在的毫秒值

Date类

Util.Date
Date()空参构造器
toString() 显示当前年月日时分秒
getTime() 获取当前Date对象对电影的毫秒数
Date(创建指定毫秒数的Date对象)

Util.Date与Sql.Date转换

Date date = new Date();

java.sql.Date date2 = new java.sql.Date(date.getTime());

SimpleDateFormat日期格式化类

因为是非静态的类,需要实例化对象才能使用SimpleDateFormat sdf = new SimpleDateFormat();

  1. 实例化

    SimpleDateFormat sdf = new SimpleDateFormat();
    //参数可以是:yyyy-MM-dd hh:mm:ss
    
  2. 格式化:日期 —> 字符串

    Date date = new Date();
    String format = sdf.format(date);
    
  3. 解析:字符串 —> 日期

    String str = "20-10-04 上午11:11";//解析的格式必须和实例化的格式一样
    Date date1 = sdf.parse(str);
    

Calendar日历类

因为是非静态的类,需要实例化对象才能使用

  1. 实例化

    //方式1:创建其子类(GregorianCalendar)的对象
    //方式2:带哦用其静态方法getInstance()
    Calendar calendar = Calendar.getInstance();
    

常用方法

get():获取相关信息,calendar.get(Calendar.DAY_OF_MONTH)

set():改变相关信息,将calendar对象的值修改,可变性。calendar.set(Calendar.DAY_OF_MONTH,22);

add():修改信息,calendar.add(Calendar.DAY_OF_MONTH,-3)

getTime():日历类 —> Date,Date date = calendar.getTime();

setTime():Date —> 日历类,calendar.setTime(date);

注意:月份是从0开始

JDK8新时间类

LocalDate,LocalTime,LocalDateTime(使用频率较高)

now() 获取当前日期/时间/日期+时间 LocalDate.now()
of() 设置指定年月日,时分秒 LocalDate.of(2020.10.4)
getxxx() 获取时间 LocalDate.getMonth()
withxxx() 设置相关属性,有返回值,体现不可变性 localDate.withDayOfMonth(22)
plusxxx() 增加相关属性,有返回值,体现不可变性 localDate.plusMonths(3)
minusxxx() 减少相关属性,有返回值,体现不可变性 localDate.minusMonths(3)

Java比较器

Comparable接口的使用举例:

  1. String、包装类等实现了Comparable接口,重写了compareTo(obj)方法,给出了比较对象的逻辑,他们是进行了从小到大的排列
  2. 重写compareTo(obj)的规则
    1. 如果当前对象this大于形参对象obj,则返回正整数
    2. 如果当前对象this小于形参对象obj,则返回负整数
    3. 如果当前对象this等于形参对象obj,则返回零

总结:Comparable接口与Comparator的使用的对比

  1. Comparable接口的方式一旦指定,保证Comparable接口实现类的对象在任何位置都可以比较大小
  2. Comparator接口属于临时性的比较,什么时候需要就创建一个实现类。

自定义实现Comparable自然排序

  1. 自定义类(bean)实现Comparable接口

  2. 重写里面的compareTo()方法,就是排序的逻辑

    public int compareTo(Object o){
        if(o instanceof foods){
            Foods foods = (Foods)o;
            if(this.price > foods.price){//如果当前对象this大于形参对象obj,则返回正整数
                return 1;
            }else if(this.price < foods.price){
                return -1;
            }else{
                return 0;
                //return this.name.compareTo(foos.name) 二级排序(实现的方式和上述效果一样)
            }
        }
        throw new RuntimeException("传输的数据类型不一致")
    }
    
  3. 调用

    Foods[] arr = new Foods[5];
    arr[0] = new Foods("aaa",34);
    arr[1] = new Foods("bbb",12);
    
    Arrays.sort(arr);
    

实现Comparator定制排序

  1. 使用匿名实现类Comparator

    Foods[] arr = new Foods[5];
    arr[0] = new Foods("aaa",34);
    arr[1] = new Foods("bbb",12);
    
    Arrays.sort(arr,new Comparator(){
        public int compare(Object o1,Object o2){
            if(o1 instanceof String && o2 instanceof String){
                String s1 = (String) o1;
                String s2 = (String) o2;
                return -s1.compareTo(s2);//使用compareble实现的方法从小到大排列的反义词,即从大到小排列
            }
            //return 0;
            throw new RuntimeException("输入的数据类型不一样")
        }
    });
    System.out.println(Arrays.toString(arr));
    

System类

  • 构造器是私有的

  • 内部成员变量、方法都是stsatic的

  • 成员变量:in,out,err

  • 成员方法

    long currentTimeMillis() 返回当前计算机时间:1970.1.1毫秒数
    void exit(int status) 0:退出,非0:异常退出。用于图形化界面退出
    void gc() 请求系统进行垃圾回收,并不一定立即回收
    String getProperty(String key) 获取系统属性值

Math类

  • 都是静态方法

  • 方法参数和返回值一般都是double类型

    abs 绝对值
    sqrt 平方根
    pow(double a,double b) a的b次幂
    log 自然对数
    exp e为底的值数
    max/min(double a,double b)
    random() 返回【0.0-1.0)的随机数
    公式:取规定[a,b]的整数 (int)(Math.random()*(b-a+1)+a)
    long round(double a) double类型数据a转换为Long型(四舍五入)

枚举类

  • 类的对象只有有限个,确定的
  • 当需要定义一组常量,建议使用枚举类
  • 如果枚举类中只有一个对象,可以作为单例模式的实现方式
  • 调用:类.枚举属性

使用enum关键词创建枚举类

  • 规则
    • 使用enum关键字
    • 先提供当前枚举类对象,再声明属性
    • 多个对象之间用,隔开 末尾对象用;结束
    • 不需要重写toString方法
    • 没有Set方法
enum Season1{
    //提供当前枚举类对象,多个对象之间用,隔开 末尾对象用;结束
    SPRING("春天"),
    SUMMER("夏天");
    //声明Season对象的属性:private final修饰
    private final String seasonName;
    //私有化构造器,并给对象属性赋值
    private Srason1(String seasonName){
        this.seasonName = seasonName;
    }
    //get方法
}
values() 返回枚举类的对象数组,方便遍历所有枚举值
valueOf(String str) 返回枚举类中对应对象名是objName的对象,没有找到会报IllegalArgumentException
toString() 返回当前枚举类对象的名称

实现接口

方式一:调用任何枚举类型都会调用该方法

  1. 实现接口,重写方法

方式二:不同枚举类型调用不同方法

    SPRING("春天"){
        @Override
        public void show(){
            System.out.println("春天你好")
        }
    },
    SUMMER("夏天"){
        @Override
        public void show(){
            System.out.println("夏天你好")
        }
    };

注解

jdk5.0新增功能

  1. 生成文档相关注解
  2. 在编译时进行格式检查(重写、过时、抑制警告)
  3. 跟踪代码依赖性,实现代替配置文件功能
@SuppressWarnings 抑制编译器警告

自定义Annotation

  • 规则
    • 使用@interface关键字
    • 内部定义成员,通常使用value表示
    • 可以指定成员的默认值,使用defalut定义
    • 如果自定义注解没有成员,表明是一个标识作用
    • 如果有成员,在使用时需要指定value值

元注解

对现有的注解进行解释说明的注解

  1. Retention:指定所修饰的注解生命周期:SOURCE编译前\CLASS编译后(默认行为)\RUNTIME运行后
    1. 只有声明为RUNTIME生命周期的注解,才能通过反射来获取
  2. Target:用于指定被修饰的注解能用于修饰哪些程序元素
  3. Documented:所修饰的注解在被javadoc解析时,保留下来
  4. Inherited:被其修饰的注解将具有继承性

JDK8新特性

  1. 可重复注解:
    1. 在注解类上声明@Repeatable,成员值为MyAnnotation.class
    2. 在MyAnnotation的Target和Retention和MyAnnotations,Inherited相同
  2. 新类型注解(Target)
    1. ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中
    2. ElementType.TYPE_USE 标识该注解能写在使用类型的任何语句中

集合框架

概述

数组在存储多个数据的特点

  1. 一旦初始化之后,长度和元素的类型都已经确定

数组在存储多个数据的缺点

  1. 初始化之后,长度不能修改
  2. 数组提供的方法有限,对增删插操作非常不便,效率不高
  3. 获取数组中元素的个数,数组没有现成的方法和属性可用
  4. 数组存储的特点:有序、可重复。对无序,不可重复的需求不能满足

体系

  • Collection接口:单列数据
    • List:元素有序、可重复(“动态”数组)
    • Set:元素无序,不可重复
  • Map接口:双列数据

实线:继承关系,虚线:实现关系

Collection

因为是接口,需要创建其实现类调用这些方法api

注意:对实现类对象中添加数据obj时,要求对obj所在类重写equals()方法

add(Object obj) 增加元素到集合中
addAll(List l) 将集合中的元素添加到当前集合中
size() 获取增减的元素个数
boolean isEmpty() 判断当前集合是否为空
clear() 清空集合元素,并不是空指针
boolean contains(Object obj) 判断集合中是否存在该元素(查的是内容,判断时会调用obj对象的equals()方法)
boolean containsAll(Collection c) 判断形参集合中所有元素是否都存在于当前集合中
remove(Object obj) 移除数据,(同样底层调用obj对象的equals()方法)
removeAll(Collection c) 从当前集合中移除c集合中的元素
Collection retainAll(Collection c) 获取当前集合和c集合的交集,并返回集合
equals(Object o) 比较两个集合中每个对象是否相同
hashCode() 返回当前对象的哈希值
Object[] toArray() 集合 —> 数组
Arrays.asList(Array a[]) 数组 —> 集合,注意:Arrays.asList(new int[]{1,2})返回的是一个地址,数组个数也是1,需要将int换成Integer
iterator iterator() 迭代器,返回一个iteractor接口的实例,用于遍历集合元素

Iterator迭代器

是一个接口,需要集合调用iterator()方法来获得迭代器接口实例。

它只是一个迭代器,并不是容器,类似指针,不会创建新的集合

每次调用iterator()方法都会得到一个全新的迭代器对象

next()方法做了两件事:1.指针下移,2.将下移之后指向的元素返回

Object next() 获取集合中的一个元素(1.指针下移,2.将下移之后指向的元素返回)
boolean hasNext() 判断是否还有下一个元素
remove() 移除迭代器指针指向的元素
//遍历集合
Collection col1 = new ArrayList();
Iterator it =col1.iterator();//获得迭代器实例
while(it.hasNext()){
    //如果还有元素,则进入循环
    System.out.println(it.next());
}

//错误方式1:跳着输出
while((it.next())!=null){//下移了一次
    System.out.println(it.next());//又下移一次,当最后一次会出现没有找到数据情况
}
//错误方式2:新迭代器,死循环
while(col1.iterator().hasNext()){
    System.out.println(col1.iterator().next());//创建新的迭代器对象,只会读出第一行数据
}
//remove()
Collection col1 = new ArrayList();
Iterator it =col1.iterator();//获得迭代器实例
while(it.hasNext()){
    if("Tom".equals(it.next())){//判断该值是否为tom,小细节:把字符串放在前面,不用对象放前边可以防止空指针异常,代码健壮性更好
        it.remove();
    }
}

注意:在还没有next()之前,不要调用remove(),会报IllegalStateException异常

增强for循环(for each)

格式:for(集合元素的类型 局部变量:集合对象)

本质也是迭代器

for(Object obj : col1){
    System.out.println(obj);
}

注意:使用for each循环给数组赋值时,不会将原数组的值相等

List

  • 元素有序、可重复

ArrayList、LinkedList、Vector三者的异同

  1. 同:三个类都实现了List接口,存储数据的特点相同:存储有序、可重复

不同:

  1. ArrayList:作为List接口主要实现类;线程不安全的,效率高;底层使用Object[]数组存储
  2. LinkedList:对于频繁插入、删除操作,使用此类效率比Arraylist高;底层使用双向链表存储
  3. Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] 数组存储(扩容2倍)

ArrayList源码分析

jdk7情况下
ArrayList list = new ArrayList();//底层创建了长度为10的Object[]数组
list.add(1);//elementData[0] = new Integer(1);
...//如果此次添加导致底层数组容量不够,则扩容
    //默认情况下,扩容为原来的容量1.5倍,同时需要将原来的数组复制到新数组中

结论

  1. 初始化长度为10,容量不够则扩容1.5倍
  2. 开发中建议使用带参构造器,将长度直接写上去,避免扩容,ArrayList list = new ArrayList(int xxx)
jdk8之后
ArrayList list = new ArrayList();//底层Object[] 初始化为{},并没有创建长度
list.add(123)//第一次调用add()时,底层才创建了长度为10的数组,并将数据添加到数组当中
...//后续扩容问题跟jdk7无异

结论:

  1. 初始化长度为0,只有在第一次增加操作时,数组才初始化为10

LinkedList源码分析

LinkedList list = new LinkedList();//内部声明了Node类型的first和last属性,默认值为null
list.add(123);//将123封装进node中,创建了Node对象
//其中,Node定义为:体现了LinkedList的双向链表的说法

List常用方法

void add(int index,Object obj) 在index位置插入元素
boolean addAll(int index,Collection c) 从Index位置开始将c中所有元素添加进来
Object get(int index) 获取指定Index位置的元素(从0开始)
int indexOf(Object obj) 返回Obj在集合中首次出现的位置
int lastIndexOf(Object obj) 返回Obj在集合中最后一次出现的位置
Object remove(int index) 移除指定Index位置的元素,并返回此元素
Object set(int index,Object obj) 设置指定index位置的元素为obj
List subList(int fromIndex,int toIndex) 返回从fromIndex到toIndex位置的子集合

总结:

增:add(Object obj)

删:remove(int index)/remove(Object),默认是索引,如果想删元素需要装箱new Integer(value)

改:set(int index,Object o)

查:get(int index)

插:add(int index,Object o)

遍历:

  1. Iterator 迭代器
  2. 增强for
  3. 普通循环

Set

  • 无序的,不可重复的
    • 无序性:不等于随机性。存储的数据在底层数组中,并非按照数组索引的顺序增加,而是通过数据的哈希值决定的
    • 不可重复性:保证添加的元素按照equals()判断时,不能返回true。即:相同的元素只能添加一个
  • Set没有额外的方法,都是用的Collection中的方法

HashSet、LinkedHashSet、TreeSet之间的异同

  1. HashSet:作为Set接口的主要实现类;线程不安全的;可以存储Null值
    1. LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照插入的顺序进行排序,对于频繁遍历的操作,它的效率比HashSet高一些
  2. TreeSet:可以按照指定属性,进行排序

HashSet底层分析

底层:数组+单向链表

增加元素的过程:以HashSet为例

我们向HashSet中添加元素a,首先会调用元素a所在类的**hashCode()**方法,计算出元素a的哈希值

此哈希值接着通过某种算法计算出在hashSet底层数组中的存放位置(索引位置),

判断数组此位置上是否有元素

如果没有元素,则添加成功—-情况1

如果位置上有其他元素(或以链表存在多个元素),则比较元素a和所在位置元素的哈希值

如果哈希值不相等,则添加成功—-情况2

如果哈希相等,则进一步调用元素a所在类的equals()方法:

equals()返回为true,证明两个值相同,则添加失败

equals()返回为false,则元素a添加成功—-情况3

  • 对于情况2,3:元素a于该索引位置上的其他元素是以单向链表的形式存储的
  • jdk7:新元素放在数组中,指向旧元素
  • jdk8:旧元素仍然在数组中,指向新元素
  • 总结:七上八下

要求:

  1. 向Set中添加的数据,其所在类一定要重写hashCode()和equals()方法
  2. 重写的hashCode()和equals()尽可能保持一致性:相同内容的对象,哈希值也相同
    • 一般直接用系统生成的就可以保证一致性

LinkedHashSet底层分析

作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据

优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet

TreeSet底层分析

  • 插入数据,要求是相同类的对象

  • 两种排序方式:自然排序(数据对应类实现Comparable接口)和定制排序

  • 自然排序中,比较两个对象是否相同的标准:compareTo()返回0(实现自然排序必须重写该方法),不再是equals(),即只按规则进行排序,不按内容(插入时规则相同,就不能插入)

    public int compareTo(Object o){
        if(o instanceof User){
            User user = (User)o;
            int compare = -this.name.compareTo(user.name);
            if(compare != 0){//内容不相同
                return compare;//返回正负值,来进行排序
            }else{//内容相同,按下一个内容进行排序
                return Integer.compare(this.age,user.age);
            }
        }else{
            throw new RuntimeException("输入类型不匹配");
        }
    }
    
  • 定制排序

    Comparator com = new Comparator(){
        @Override
        public int compare(Object o1,Object o2){
            if(o1 instanceof User && o2 instanceof User){
                User u1 = (User) o1;
                User u2 = (User) o2;
                return Integer.compare(u1.getAge(),u2.getAge());
            }else{
                throw new RuntimeException("输入的数据类型不匹配");
            }
        } 
    }
    TreeSet set = new TreeSet(com);//传入参则使用定制排序,否则使用自然排序
    

集合面试题

  1. 集合Collection中存储的如果是自定义类的对象,需要自定义类重写哪些方法?为什么?

    equals()方法。 在调用contains()/remove()等方法时,都会调用equals方法
    List:equals()方法
    Set:(HashSet、LinkedHashSet为例):equals()、hashCode()
        (TreeSet为例):Comparable:compareTo(Object obj)
                        Comparator:compare(Object o1,Object o2)
    
  2. ArrayList,LinkedList,Vector三者的相同点与不同点

    相同:都实现于List接口,数据结构的特点相同:有序、可重复
    不同:
    ArrayList:是最主要的实现类,底层是数组存储,线程不安全,效率高,底层数组扩容是以前的1.5倍
    Vector:是比较古老的实现类,底层是数组存储,线程安全,效率较低,底层数组扩容是以前的2倍
    LinkedList:底层是双向链表,插入、删除的效率高。
    
    源码:
    ArrayList底层源码在jdk8之后有所改动,在jdk8之前,new一个ArrayList会创建一个长度为10的数组,之后的版本new只会创建一个空的数组,当第一次调用add()时,才会创建长度10的数组,并且库容都是1.5倍
    LinkedList:内部声明的都是Node类型,不仅存有数据,还存有上一个node和下一个Node的数据。
    Vector:底层也是数组,长度为10没有变动,扩容是2倍
    
  3. List和Set的区别

    相同:都实现于Collection接口
    不同:
    List:有序可重复
    Set:无序不可重复
    
  4. List接口有哪些常用方法?

    增:add(Object obj):remove(Object obj)/remove(int index):set(int index,Object obj)
    查:get(int index)
    插:add(int index,Object obj)
    长度:size()//元素的个数
    遍历:iterator,foreach,普通for(有索引,Set和Colleciton不可以使用)
    
  5. 如何使用Iterator和增强for循环遍历list

    Iterator it = list1.iterator();
    while(it.hasNext()){
        System.out.println(it.next());
    }
    
    for(Object obj : list1){
        System.out.println(obj);
    }
    
  6. Set存储数据的特点是什么?常见的实现类有什么?说明一下彼此的特点

    特点:无序不可重复
    实现类:HashSet、LinkedHashSet、TreeSet
    HashSet:是Set的主要实现类,线程不安全,可以存储Null值
    LinkedHashSet:是HashSet的主要实现类,在内部存储时会记录插入的顺序,遍历时可以按照插入顺序进行排序,遍历时效率高一些
    TreeSet:可以按照指定的属性进行排序
    
  7. 判断结果

    public void test(){
        HashSet set = new HashSet();
        Person p1 = new Person("1001","aa");
        Person p2 = new Person("1002","bb");
        set.add(p1);
        set.add(p2);
        System.out.println(set);//2个
        p1.name = "cc";//此时1001cc仍然保存在1001aa的位置
        set.remove(p1);//此时找的是1001cc的哈希值,但更改后的cc仍然在aa的位置,故没有删除
        System.out.println(set);//2个
        set.add(new Person(1001,"cc"));//此时找的是1001cc的哈希值,故插入成功
        System.out.println(set);//3个,bb,cc,cc
        set.add(new Person(1001,"aa"));//此时找的是1001aa的哈希值,虽然位置上有cc,但是equals()方法比较两个不一样,故用链表存储成功
         System.out.println(set);//4个,bb, cc,aa ,cc
    }
    

Map

双列数据,存储Key-value对的数据

不同实现类的区别

HashMap:作为Map的主要实现类;线程不安全的,效率高;存储Null

–LinkedHashMap:保证在添加Map元素时,可以按照添加的顺序实现遍历(在原有HashMap结构上,增加了一对指针,指向前一个和后一个元素),对于频繁的遍历操作,此类效率高

TreeMap:保证按照添加的Key-value进行排序,按照(key)来排序。此时考虑key的自然排序和定制排序 。底层使用的红黑树

Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null

–Properties:常用来处理配置文件,key-value都是String类型

HashMap的底层:数组+链表(jdk7及以前)。数组+链表+红黑树(jdk8)

面试题

  1. HashMap的底层实现原理?
  2. HashMap和Hashtable的异同?
  3. CurrentHashMap与Hashtable的异同?

Map结构的理解

key:无序的、不可重复的,使用Set存储所有的Key —> key 所在类要重写equals()和hashCode()

value:无序的、可重复的,使用Collection存储所有的value —>value所在的类药重写equals()

entry:就是一个键值对,无序的、不可重复的,使用Set存储所有的entry

HashMap的底层实现原理?

JDK7为例

HashMap map = new HashMap();

在实例化以后,底层创建了长度为16的一维数组Entry[] table

map.put(key1,value1);

首先,调用key1所在类的hashCode()计算key1的哈希值,此哈希值经过某种算法之后,得到Entry数组所在的位置

如果该位置数据为空,则key1-value1添加成功。—情况1

如果该位置数据不为空(意味着此位置上存在一个或多个数据(以链表的形式存在)),比较Key1和一个或多个数据的哈希值

如果key1的哈希值和其他数据哈希值不相同,则key1-value1添加成功。—情况2

如果key1的哈希值和其他数据哈希值相同,则继续**比较equals(key)**,

如果equals返回为false(不相同)则添加成功。—情况3

如果equals返回为true:使用value1替换原有value值(修改的作用)。

补充:情况2,3存储的数据是以链表的形式存储。

如果涉及到扩容问题,当超出临界值(且要存放的位置有值)时,扩容为原来容量的2倍,则并将原有数据复制过来。如果到临界值,要存放的位置无值,则继续往后加值。

JDK8相较于7在底层实现方面的不同

  1. new HashMap():底层没有创建一个长度为16的数组

  2. Jdk8 底层的数组是:Node[],而非Entry[]

  3. 首次调用put()方法时,底层创建长度为16的数组

  4. jdk7底层结构只有:数组+链表。Jdk8中底层结构:数组+链表+红黑树

    当数组某个索引位置上的元素以链表形式存在的数据个数>8,且数组长度>64时

    此时该索引位置上的所有数据改为使用红黑树存储

DEFAULT_INITIA_CAPACITY:HashMap的默认容量:16

DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75(小会导致数据碰撞小,数组空间经常会扩容,链表短;大会导致数据碰撞高,链表长,影响查询和插入)决定了HashMap的数据密度

threshold:扩容的临界值,容量 * 加载因子:16*0.75=12(达到该临界值(且后续有值)就会扩容)

TREEIFY_THRESHOLD:链表长度大于该默认值,转化为红黑树:8

MIN_TREEIFY_CAPACITY:Node被树化时最小的hash表容量:64

LinkedHashMap底层实现原理(了解)

多了before和after来记录存储顺序

Map接口的方法

增加、删除、修改
Object put(Object key,Object value) 添/改:将指定Key-value增加(或修改)到map中
void putAll(Map m) 将m中的所有key-value存放到当前Map中
Object remove(Object key) 删:移除指定key的Key-value,并返回value
void clear() 清空当前Map中的所有数据
元素查询
Object get(Object key) 查:获取指定key对应的value
boolean containsKey(Object key) 是否包含指定的key
boolean containsValue(Object value) 是否包含指定的value
int size() 长度:返回map中Key-value的个数
boolean isEmpty() 判断当前Map是否为空
boolean equals(Object obj) 判断当前map和参数对象obj是否相等(也得是Map)
元视图操作 遍历:
Set keySet() 返回所有Key构成的Set集合
Collection values() 返回所有value构成的collection集合
Set entrySet() 返回所有key-value构成的Set集合
//遍历entry的set集合,方式1
Set entrySet = map.entrySet();
Iterator it = entrySet.iterator();
while(it.hasNext()){
    Object obj = it.next();
    //entrySet集合中的的元素都是entry
    Map.Entry entry = (Map.Entry) obj;
    System.out.println(entry.getKey()+":"+entry.getValue());
}

//方式2
Set set = map.keySet();
Iterator it = set.iterator();
while(it.hasNext()){
    Object key = it.next();
    Object value = map.get(key);
    System.out.println(key+":"+value);
}

TreeMap的使用

  • 向TreeMap中添加key-value,要求key必须是由同一类创建的对象

  • 因为要按照key进行排序:自然排序、定制排序

    //自然排序,key所在类必须实现comparable接口
    public void test(){
        TreeMap map = new TreeMap();
        User u1 = new User("Tom",23);
        User u2 = new User("Jerry",13);
        map.put(u1,98);
        map.put(u2.99);
        //遍历
        Set entrySet = map.entrySet();
        Iterator it = entrySet.iterator();
        while(it.hasNext()){
            Object obj = it.next();
            //entrySet集合中的的元素都是entry
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey()+":"+entry.getValue());
        }
    }
    //定制排序
    public void test1(){
        Tree map = new TreeMap(new Comparator(){
            @Override
            public int compare(Object o1,Object o2){
                if(o1 instanceof User && o2 instanceof User){
                    User u1 = (User) o1;
                    User u2 = (User) o2;
                    return Integer.compare(u1.getAge(),u2.getAge());//按照年龄从小到大排序
                }
                throw new RuntimeException("输入的类型不匹配");
            }
        });
        //后面都一样  
    }
    

面试题

  1. Map存储数据的特点是什么?并指明key、value、entry存储数据的特点

    双列数据,存储key-value数据
    key:无序的、不可重复的---Set
    value:无序的、可重复的---collection
    key-value:无序、不可重复---Set
    
  2. 描述HashMap的底层实现原理

    在第一次初始化时,并不会常见出长度为16的node数组,只有在第一次调用Put方法,才会真正创建出长度为16的数组。
    调用put方法存key-value时,会将key调用hashCode()方法计算出哈希值,再经过某种算法计算出存储在node数组中的位置。
    如果该位置没有值,则添加成功----情况1
    如果该位置有值(有可能是一个或多个数据组成的链表),则比较哈希值
    如果哈希值不同,则添加成功,通过链表的方式存储在数组中----情况2 
    如果哈希值相同,则调用key所在类的equals()方法进行比较
    如果返回为false,则添加成功----情况3
    如果返回为true,则将新的value值替换为旧value值
    
    HashMap的底层存储结构是:数组+链表+红黑树
    当数组上的某个位置上的链表长度超过8且数组的总长度大于64,则将该链表变为红黑树存储
    
    当涉及到数组扩容问题时,HashMap底层有临界值,当要存储的数据超过临界值(且存储的位置有值时),会将数组扩容为原来的2倍,并将原来的数据复制进新的数组当中。
    
  3. Map中常用实现类有哪些?各自有什么特点?

    HashMap:是Map的主要实现类,线程不安全的,效率高,可以存储Null值
    LinkedHashMap:保证在存储key-value时,可以根据添加的顺序进行遍历,在HashMap的基础上,增加了两个指针,分别指向前后数据,在频繁遍历时效率更高。
    TreeMap:保证在存储key-value时,根据key进行排序,具体排序规则根据Key的自然排序和定制排序 。底层使用的是红黑树
    Hashtable:是Map的古老实现类,线程安全,效率低,不能存储Null值
    Properties:存储方式也是key-value,键值对都是String类型,用于配置文件
    
  4. LinkedHashMap的底层实现原理

    底层和HashMap的机构相同,因为LinkedHashMap继承了HashMap,区别在于LinkedHashMap内部提供了Entry,替换了Node类型,其中增加了before和after用于存储上一个和下一个数据,更有利于遍历。
    
  5. 如何遍历map,代码实现

    Set set = map.keySet();
    Iterator it =set.iterator();
    while(it.hasNext()){
        Object key = it.next();
        Object value = map.get(key);
        System.out.println(key+":"+value);
    }
    
  6. Collection和Collections的区别?

    Collection是一个存储单列数据的接口,内部包括List和Set
    Collections是操作Collection和Map的常用工具类
    
  7. TreeMap的使用?

    向TreeMap中添加数据,要求key的类型保持一致
    因为要按照key进行排序:自然排序、定制排序
    

Collections工具类

面试题:Collection和Collections的区别?

Collection是一个存放单列数据的接口,而Collections是操作Collection、Map的工具类

常用方法

排序
reverse(List) 反转List中元素的顺序
shuffle(List) 对List集合元素进行随机排序
sort(List) 根据元素的自然顺序对指定List集合元素按升序排序
sort(List,Comparator) 根据指定Comparator产生的顺序对List集合元素进行排序
swap(List,int index1,index2) 将指定List集合中的两个元素进行交换
查找、替换
Object max(Collection) 根据元素自然排序,返回最大元素
Object max(Collection,Comparator) 根据Comparator指定的顺序,返回给定集合的最大元素
Object min(Collection) 最小
Object min(Collection,Comparator) 最小
int frequency(Collection,Object) 返回指定集合指定元素的出现次数
void copy(List dest,List src) 将src中的内容赋值到dest中
boolean replaceAll(List,Object old,Object new) 把list中的旧值换成新值
//copy()方法的问题
//错误示范:
List dest = new ArrayList();
Collections.copy(dest,list);
//正确操作
List dest = Arrays.asList(new Object[list.size()]);
Collections.copy(dest,list);

同步控制

Collections类中提供了多个**synchronizedXxx()**方法,可以让指定集合包装成线程同步的集合。

ArrayList list = new ArrayList(); List list=Collections.synchronizedList(list);

数据结构:

顺序表(Array,ArrayList)

特点:

  1. 使用连续分配的内存空间
  2. 一次申请一大段连续的空间,需要实现声明最大可能要占用的固定内存空间

优点:

设计简单,读取与修改表中任意一个元素的时间都是固定的

缺点:

  1. 容易造成内存浪费
  2. 删除或插入需要移动大量数据

链表(LinkedList)

特点:

  1. 使用不连续的内存空间
  2. 不需要提前声明好指定大小的内存空间

优点:

  1. 充分节省内存空间
  2. 数据的插入与删除方便,不需要移动大量数据

缺点:

  1. 设计麻烦
  2. 查找数据必须按照顺序找到该数据为止

泛型

  • 泛型不能是基本数据类型,需要使用其包装类

  • 在jdk5.0时,集合接口和集合类都修改为带泛型结构

  • 在实例化集合时,可以指明具体泛型结构

  • 指明之后,集合类或接口中凡是定义类或接口时,内部结构使用到泛型都和定义的相同

  • 如果实例化时,没有指明泛型的类型,默认为Object类型

  • jdk7:类型推断HashMap<User> map = new HashMap<>();

//指明泛型的comparable接口
public User implements Comparable<User>{//指定泛型
    @Override
    public int compareTo(User u){//因为指明了泛型,形参直接改变,不需要判断类型了
        return this.name.compareTo(u.name);
    }
}

自定义泛型类

  1. 定义泛型类,建议在实例化时指明类的泛型

    如果没有指明,则认为泛型类型为Object类型

    构造器不需要加<>,但是实例化需要加

    public Order<T>{//定义泛型类
        T orderT;
        public Order(T orderT){
            this.orderT = orderT;
        }
    }
    public void test(){
        Order<String> order = new Order<>("123");//指明泛型类型,则参数就是什么类型
    }
    
  2. 子类继承父类时声明泛型,则泛型就是什么类型

    public class OrderChildren extends Order<Integer>{
    }
    public void test(){
        OrderChildren child = new OrderChildren(123);//因为继承父类指定泛型,则参数就是什么类型
        //注意:子类只是一个普通类,不需要加<>
    }
    
  3. 定义泛型类,子类继承父类时也没有声明泛型,建议在实例化时指明类的泛型

    public class OrderChildren<T> extends Order<T>{
    }
    public void test(){
        OrderChildren<String> child = new OrderChildren<>("123");//泛型类指明类型,则参数就是什么类型
    }
    
  • 泛型在继承方面的体现:

    • 类A是类B的父类,G和G二者不具备子父类关系:

      • List<Object> list1; List<String> list2,list2不能赋值给List1
    • 类A是类B的符类,A,B可以赋值

      • List<String> list1; ArrayList<String> list2,可以赋值
    • 通配符:类A是类B的父类,G和G二者不具备子父类关系,G<?>是其父类,可以赋值

      • 添加:对于List<?>不能向其内部添加数据

      • 获取:允许读取数据,读取的数据类型为Object

      • 有限制的通配符

        List<? extends Person> list1 = null;//泛型是person子类
        List<? super Person> list2 = null;//泛型是person父类
        
        List<Student> list3 = null;
        List<Person> list4 = null;
        List<Object> list5 = null;
        
        list1 = list3;
        list1 = list4;
        list1 = list5;//报错,extend表示是其子类
        
        list2 = list3;//报错,super表示是其父类
        //获取值
        Student s = list1.get(0);//报错,最小为Person
        Object o = list2.get(0);//正确,最小为object
        

自定义泛型方法

泛型方法的泛型和泛型类的泛型没有关系,换句话说泛型方法还可以在普通类中声明

  • 静态方法不能使用类的泛型
  • 泛型方法,可以声明为静态。原因:泛型参数是在调用方法时确定的,并非实例化时确定
public class Order<T>{
//泛型类是T,泛型方法是E,没有任何关系
    public <E> List<E> copy(E[] arr){
        ArrayList<E> list = new ArrayList<>();
        for(E e: arr){
            list.add(e);
        }
    }
}
public void test(){
    Order<String> order = new Order<>();
    Integer[] arr = new Integer[]{1,2};
    List<Integer> list= order.copy(arr);//泛型方法的泛型取决于传入的参数,与实例化无关
    System.out.println(list);
}

面试题

  1. 如何遍历带泛型的key,value,map

    //遍历key
    Map<String,Integer> map = new HashMap<>();
    Set<String> keys = map.keySet();
    for(String key:keys){
        System.out.println(key);
    }
    //遍历value
    Map<String,Integer> map = new HashMap<>();
    Collection<Integer> values = map.values();
    Iterator it = values.iterator();
    while(it.hasNext()){
        System.out.println(it.next());
    }
    //遍历key-value
    Map<String,Integer> map = new HashMap<>();
    Set<Map.Entry<String,Integer>> entrys = map.entrySet();
    for(Map.Entry<String,Integer> entry:entrys){
        String key = entry.getKey();
        Integer value = entry.getValue();
        System.out.println(key+"---"+value);
    }
    
  2. 提供一个方法,用于遍历HashMap<String,String>中的所有value,并放在list集合中,用上泛型

    public List<String> getList(HashMap<String,String> map){
        Collection<String> values = map.values();
        List<String> list = new ArrayList<>();
        for(Stirng value:values){
            list.add(value);
        }
        return list;
    }
    

IO流

File类

  • java.io.File:一个对象,文件和文件目录路径的抽象表达形式
  • 只涉及到关于文件的操作,并没有涉及到内容的操作。如果需要读写内容,就需要IO流来完成

创建File类的实例

并不会创建出真正的文件,只是将对象加载进内存中

  • IDEA中,Junit单元测试方法中,相对路径指的是当前模块下;main()测试中,指的是总工程下
File(String filePath)绝对路径/相对路径 File file1 = new File(“hello.txt”),File file2 = new File(“d:\hello.txt”)
File(String parentPath,String childPath)父,子目录 File file3 = new File(“D:\ “,”workspace”)
File(File paretPath,String childPath) File file4 = new File(file3,”hello.txt”);

常用方法

String getAbsolutePath() 获取绝对路径
String getPath() 获取路径
String getName() 获取名称
String getParent() 获取上层文件目录路径,若无,返回null
long length() 获取文件长度(字节),不能获取目录长度
long lastModified() 获取最后一次修改时间,毫秒值
String[] list() 获取指定目录下的所有文件或文件目录的名称数组
File[] listFiles() 获取指定目录下的所有文件或文件目录的File数组
boolean renameTo(File dest) 把文件重命名为指定的文件路径(要想保证返回true,需要file1在硬盘中存在的,且file2不能在硬盘中存在)

判断功能

boolean isDirectory() 判断是否是文件目录
boolean isFile() 判断是否是文件
boolean exists() 判断是否存在

创建、删除功能

boolean createNewFile() 创建文件,若文件存在则不创建
boolean mkdirs() 创建文件目录,如果上层文件目录不存在,一并创建
boolean delete() 删除文件

站位:(我们是站在程序(内存)的角度)

  • 输入:将文件写入的内存当中

  • 输出:将内存中的数据存入磁盘

流的分类

  • 数据单位:字节流(8bit)、字符流(16bit)

  • 流向:输入流、输出流

  • 角色:节点流(File)、处理流(缓冲流是其中的一种)

读入数据

从硬盘文件中读取数据到内存中

字符流
//1.实例化File类的对象,指明要操作的对象
File file = new File("hello.txt");
//2.提供具体的流
FileReader fr = new FileReader(file);
//3.数据的读入
//read():返回读入的一个字符,如果达到文件末尾,返回-1
int data = fr.read();//获取的是char转码后的int
while(data != -1){
    System.out.println((char)data);
    data = fr.read();//获取下一个数据
}
//流的关闭操作
fr.close();

说明:

  1. read()的理解:返回读入的一个字符,如果到达末尾,返回-1
  2. 异常处理:为了保证流资源一定可以执行关闭操作,需要使用try-catch-finally(并且判断一下流是否为null)
  3. 读入的文件一定要存在,否则就会报FileNotFoundException

read(char[ ] cbuf)一次读入多个数据

//1.实例化File类的对象,指明要操作的对象
File file = new File("hello.txt");
//2.提供具体的流
FileReader fr = new FileReader(file);
//3.数据的读入
//read(char[ ] cbuf)一次读入数组长度的数据,如果达到文件末尾,不足长度则返回-1
char[] cbuf = new char[5];//存放数据的地方
int len;//记录数据长度
//方式1
while((len=fr.read(cbuf))!=-1){
    for(int i = 0;i<len;i++){
        System.out.println(cbuf[i]);
    }
}
//方式2
//String str = new String(cbuf,0,len);
//System.out.println(str);
字节流

FileInputStream

  • 操作也是四步骤,1.造文件,2.造流,3.读方法,4.关流

  • 多数据读用byte[]数组

  • 使用字节流处理文本文件可能(在内存中读时)出现乱码:中文

写出数据

从内存中写出数据到硬盘文件里

  1. 输出操作,对应的File可以不存在
    1. 如果不存在,回自动创建此文件
    2. 如果存在:
      1. 如果使用流构造器为:FileWriter(file/false)/FileWriter(file)对原有的文件覆盖
      2. 如果流使用构造器为:FileWriter(file/true):不会对原有文件覆盖,而是在原有文件中追加内容
字符流
//1.实例化File类的对象,指明要操作的对象
File file = new File("hello.txt");
//2.提供具体的流
FileWriter fw = new FileWriter(file,false);//默认为false
//3.写出的操作
fw.write("hello");
fw.write("nihao");
//4.流资源的关闭
fw.close();

练习:读写数据

//1.实例化File类的对象,指明要操作的对象
File file1 = new File("hello.txt");
File file2 = new File("hello2.txt");
//2.提供具体的流
FileReader fr = new FileReader(file1);
FileWriter fw = new FileWriter(file2);
//3.数据的读入和写出操作
char[] cbuf = new char[5];
int len ;
//读操作
while((len = fr.read(cbuf))!=-1){
    //写入操作
    fw.write(cbuf,0,len);//每次读取len个数据
}
//4.流的关闭
fw.close();
fr.close();
字节流

FileOutputStream

  • 操作也是四步骤,1.造文件,2.造流,3.写方法,4.关流

  • 写可以用字节流,因为不涉及到在内存中读

缓冲流

作用:用来提高读写的速度

  • BufferedInputStream
  • BufferedOutputStream
  • BufferedReader
  • BufferedWriter
  • 在具体流嵌套一层缓冲流
  • 关流:先关外层再关内层,外层关了内层就会自动关闭

读写操作

//1.实例化File类的对象,指明要操作的对象
File file1 = new File("hello.txt");
File file2 = new File("hello2.txt");
//2.提供具体的流
FileReader fr = new FileReader(file1);
FileWriter fw = new FileWriter(file2);
//3.提供缓冲流
BufferedReader br = new BufferedReader(fr);//传入具体的流
BufferedWriter bw = new BufferedWriter(fw);
//数据的读入和写出操作
while((len = br.read(cbuf))!=-1){
    //写入操作
    bw.write(cbuf,0,len);//每次读取len个数据
}
//关流:先关外层再关内层,外层关了内层就会自动关闭
br.close();
bw.close();

readLine()读写一行的操作

将一行数据读取到字符串中,如果没有值则为null

//1.实例化File类的对象,指明要操作的对象
File file1 = new File("hello.txt");
File file2 = new File("hello2.txt");
//2.提供具体的流
FileReader fr = new FileReader(file1);
FileWriter fw = new FileWriter(file2);
//3.提供缓冲流
BufferedReader br = new BufferedReader(fr);//传入具体的流
BufferedWriter bw = new BufferedWriter(fw);
//4.读写操作
String data ;
while((data=br.readLine())!=null){
    bw.write(data+"\n");//方式1换行
    //方式2换行
    //bw.write(data);
    //bw.newLine();
}

练习题

  1. 实现文件的复制,使用缓冲流

  2. 实现文件的加密操作:for(i = 0;i<len ;i++){cubf[i] =(byte) (cubf[i]^5);}

    1. 解密只需要对加密对象再异或5就可以实现解密
  3. 统计一个文件中所有字符出现的次数

        public static void main(String[] args) {
            BufferedReader br = null;
            BufferedWriter bw = null;
            try {
                //创建文件对象
                File file = new File("F:\\123.txt");
                File file2 = new File("F:\\1234.txt");
                //存放字符和出现的次数
                Map<Character,Integer> map = new HashMap<>();
                //创建具体流
                FileReader fr = new FileReader(file);
                FileWriter fw = new FileWriter(file2);
                //创建缓冲流
                br = new BufferedReader(fr);
                bw = new BufferedWriter(fw);
                //读操作
                int len;
                while((len= br.read())!=-1){
                    //将len还原成char
                    char ch = (char)len;
                    if(!map.containsKey(ch)){
                        //字符第一次出现
                        map.put(ch,1);
                    }else{
                        //字符出现过,增加1次
                        map.put(ch,map.get(ch)+1);
                    }
                }
                //遍历map集合
                Set<Map.Entry<Character,Integer>> set = map.entrySet();
                for (Map.Entry<Character,Integer> entry:set) {
                    switch (entry.getKey()){
                        case ' ':
                            bw.write("空格="+entry.getValue());
                            break;
                        case '\t':
                            bw.write("tab键="+entry.getValue());
                            break;
                        case '\n':
                            bw.write("换行="+entry.getValue());
                            break;
                        case '\r':
                            bw.write("回车="+entry.getValue());
                            break;
                        default:
                            bw.write(entry.getKey()+"="+entry.getValue());
                            break;
                    }
                    bw.newLine();//换行
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            finally {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    

转换流

  • 属于字符流
    • InputStreamReader:将一个字节输入流转换为字符输入流,父类Reader(看后缀)
    • OutputStreanWriter:将一个字符的输出流转换为字节输出流,父类Writer
  • 作用:字节与字符之间的转换
  • 解码:字节—>字符:InputStreamReader
  • 编码:字符—>字节:OutputStramWriter

InputStreamReader

注意:具体用哪个字符集去读,取决于原来的文件编码是什么

FileInputStream fis = new FileInputStream("123.txt");//字节流
//InputStreamReader isr = new InputStreamReader(fis);//使用系统默认字符集
InputStreamReader isr = new InputStreamReader(fis,"UTF-8");//使用UTF-8去读
//读操作
char[] cbuf = new char[20];
int len;
while((len=isr.read(cbuf))!=-1){
    String str = new String(cbuf,0,len);
    System.out.println(str);
//    for(int i=0;i<len;i++){
//        System.out.println(cbuf[i]);
//    }
}
isr.close();

转换流实现文件读写

  • utf-8—->gbk
FileInputStream fis = new FileInputStream("123.txt");//字节输入流
FileOutpuStream fos = new FileOutStream("1234.txt");//字节输出流
InputStreamReader isr = new InputStreamReader(fis,"UTF-8");//使用UTF-8去读
OutputStreamWriter osw = new OutputStreamWriter(fos,"gbk");//使用gbk去写

字符编码集

ASCII:美国标准,1个字节7位

ISO8859-1:拉丁语表,1个字节8位

GB2312、GBK:中国标准,最多2个字节

Unicode:全世界语言编码集,2个字节,但因为要表示用1个还是2个不能很好界定,没有落地

UTF-8:变长编码方式,1-6个字节,可以根据开头的1 *0来界定用几个字节进行识别

标准流

注意:属于字节流

  • System.in
  • System.out

练习:使用System.in来输入字符串,将其变为大写

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        while(true){
            System.out.println("请输入要转换的字符串:");
            String data = br.readLine();
            if("e".equalsIgnoreCase(data)||"exit".equalsIgnoreCase(data)){
                System.out.println("程序结束");
                break;
            }
            System.out.println(data.toUpperCase());
        }
        br.close();
    }

注意:

  1. IDEA使用单元测试不能在控制台进行输入,需要使用main主程序
  2. 这里应该使用try-catch-finally,偷懒只实现了主要代码

打印流

PrintStream

PrintWriter

作用:将输出的信息输出在磁盘中

    public static void main(String[] args) throws FileNotFoundException {
        //创建输出流,指定保存的位置
        FileOutputStream fos = new FileOutputStream("F:\\out.txt");
        //创建打印流,flush为true,表示换行就会自动刷新
        PrintStream ps = new PrintStream(fos,true);
        if(ps!=null){
            //把标准输出流改为打印流的方式输出(即将控制台输出改为文件输出)
            System.setOut(ps);
        }
        //输出逻辑
        for(int i = 0;i<255;i++){
            char data = (char)i;
            System.out.print(data);
            if(i%50==0){
                //每50个字符换行
                System.out.println();
            }
        }
        //关闭流
        ps.close();
    }

数据流

DataInputStream读数据

DataOutputStream写数据

  • 作用:用于读取或写出基本数据类型的变量或字符串

练习:将内存中的字符串、基本数据类型写出到文件中和读取

//写数据
 //创建具体输出流,数据流
DataOutputStream dos = new DataOutputStream(new FileOutputStream("F:\\data.txt"));
//写数据
dos.writeUTF("小明");
dos.flush();
dos.writeInt(12);
dos.flush();
//关流
dos.close();

注意:读数据的时候,读的顺序一定要和写入时一样

//读数据
DataInputStream dis = new DataInputStream(new FileInputStream("F:\\data.txt"));
dis.readUTF();
dis.readInt();
dis.close();

对象流

  • 作用:用于存储和读取基本数据类型或对象的处理流

ObjectInputStream:读到内存,反序列化

ObjectOutputStream:写到磁盘或进行网络传输,序列化

  • 自定义类(User)序列化的方法:
    1. 需要自定义类实现接口:Serializable
    2. 当前类提供一个全局常量:public static final long serialVersionUID = xxxxxxL;
      1. 算是一个唯一标识,如果没有,再序列化之后,自定义类发生了修改,那么系统将无法识别
    3. 除了当前自定义类需要实现Serializable接口,其内部所有属性也必须可序列化(默认情况下,基本数据类型是可序列化的)
    4. 补充:static/transient修饰的成员变量不能序列化

对象序列化机制:允许把Java对象转换为平台无关的二进制流,从而允许把这种二进制流持久化保存在磁盘上,或通过网络这种二进制流传输到另一个网络节点。当其他程序获取到这种二进制流,就可以恢复成原来的java对象。

//序列化,写到磁盘
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\obj.txt"));
oos.writeObject(new String("hahaha"));
oos.flush();
oos.close();

//反序列化,读到内存
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\obj.txt"));
String o = (String) ois.readObject();
System.out.println(o);
ois.close();

随机存储文件流

RandomAccessFile

  1. 直接继承于java.lang.Object,实现了DataInput和DataOutput接口

  2. 既可以作为一个输入流,又可以作为一个输出流

  3. 如果作为输出流,写出到的文件不存在时,会自动创建

    如果写出到的文件存在,则会对原有文件内容进行覆盖(默认情况下是从头开始覆盖)

  4. RandomAccessFile(File,mode),

    1. mode:
      1. r:以只读方式打开
      2. rw:以读、写
      3. rwd:读,写,同步
      4. rws:读写,同步内容+元数据
RandomAccessFile raf =new RandomAccessFile(new File("F:\\random.txt"),"r");
RandomAccessFile raf2 = new RandomAccessFile(new File("F:\\random1.txt"),"rw");
byte[] data = new byte[20];
int len;
while((len = raf.read(data))!=-1){
    raf2.write(data,0,len);
}
raf.close();
raf2.close();
RandomAccessFile raf2 = new RandomAccessFile(new File("F:\\random1.txt"),"rw");
raf2.write("hahaha".getBytes());//如果写出到的文件存在,则会对原有文件内容进行覆盖
raf2.close();

面试题

  1. 字节流与字符流的区别与使用情景

    字节流: read(byte[] buffer)/read() 非文本文件,如果只是复制文件,不在控制台输出,字节流也可以实现
    字符流: read(char[] cbuf)/read() 文本文件
    
  2. 使用缓冲流实现a.jpg复制到b.jpg

    //创建具体流
    FileInputStream fis = new FileInputStream("F:\\123.txt");
    FileOutputStream fos = new FileOutputStream("F:\\1234.txt");
    //创建缓冲流
    BufferInputStream bis = new BufferInputStream(fis);
    BufferOutputStream bos = new BufferOutputStream(fos);
    //读写操作
    int len;
    byte[] data = new Byte[1024];
    while((len = bis.read(data))!=-1){
        //写操作
        bos.write(byte,0,len);
    }
    bis.close();
    bos.close();
    
  3. 转换流是哪两个类,分别的作用是什么?

    InputStreamReader:将输入的字节流转换为输入的字符流,解码
    OutputStreamWriter:将输出的字符流转换为输出的字节流,编码
    InputStreamReader isr = new InputStreamReader(new FileInputStream("1.txt"),"utf-8");//根据文件的编码来读
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("1.txt"),"gbk");//需要转换什么编码就写什么
    
  4. 总结遍历方式

    字节流: read(byte[] buffer)/read()
    //方式1:数组写
    int len;
    byte[] data = new byte[1024];
    while((len = bis.read(data))!=-1){
        bos.write(byte,0,len);
    }
    //方式2:逐个字节写
    int len;
    byte[] data = new byte[1024];
    while((len = bis.read(data))!=-1){
        for(int i=0;i<len;i++){
            bos.write(data[i]);
        }
    }
    //方式3:字符串读行
    String data;
    while((data = bis.readLine())!=null){
        bos.write(data);
        //bos.newLine();//换行
    }
    //方式4:数组转字符串
    byte[] cbuf = new byte[1024];
    int len;
    while((len = bis.read(cbuf))!=-1){
        String data = new String(cbuf,0,len);
        bos.write(data);
    }
    
    字符流: read(char[] cbuf)/read()
    

网络编程

网络通信三要素

  1. IP:电子设备(计算机)在网络中的唯一标识。对应的类:InetAddress

    1. IPv4
    2. IPv6
  2. 端口:应用程序在计算机中的唯一标识。 0~65536

    1. 公认端口:0~1023
    2. 注册端口:1023~49151
    • 端口号与IP地址组合得到一个网络套接字:Socket
  3. 传输协议:规定了数据传输的规则

    1. 基础协议:
      1. tcp:安全协议,三次握手,进行大数据量传输。 速度稍慢
      2. udp:不安全协议。 速度快,以数据包的形式发送。只负责发,不管对方是否接收到
  4. 网络通信落地分为4层:应用层、传输层、网络层、物理+数据链路层

InetAddress实例化

IP对象实例化(没有构造方法,可以静态创造)

getByName(String host),getLocalhost()

InetAddress inet1 = InetAddress.getByName("192.168.10.14");
InetAddress inet2 = InetAddress.getByName("www.baidu.com");
InetAddress inet3 = InetAddress.getLocalhost();

方法

  • getHostName():获取域名
  • getHostAddress():获取主机地址

实现客户端-服务器交互

注意:

  1. 客户端创建的Socket是服务器端的套接字(ip+port)
  2. 服务器端需要先获取客户端的socket,才能获取流

实现消息交互

//客户端
public void client(){
    Socket socket = null;
    OutputStream os = null;
    try {
        //1.创建套接字,指明服务器IP和端口
        InetAddress inetAddress = InetAddress.getLocalHost();
        socket = new Socket(inetAddress,6666);
        //2.获取输出流
        os = socket.getOutputStream();
        //3.发送数据
        os.write("你好".getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        //4.关闭流和套接字
        if(os!=null){
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(socket!=null){
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

//服务器端
//这里偷懒将异常抛出,应该使用try-catch
public void server() throws IOException {
    //1.创建ServerSocket,指明端口
    ServerSocket ss = new ServerSocket(6666);
    //2.获取客户端的socket,表示接收来自客户端的消息
    Socket socket = ss.accept();
    //3.获取输入流
    InputStream is = socket.getInputStream();
    //4.读取操作
    byte[] data = new byte[1024];
    int len;
    while((len = is.read(data))!=-1){
        String str = new String(data,0,len);
        System.out.print(str);
        //socket.getInetAddress()获取ip对象
        System.out.println("收到来自"+socket.getInetAddress().getHostAddress()+"的消息");
    }
    //5.关闭流、socket、服务器
    is.close();
    socket.close();
    ss.close();
}

服务器端保存客户端发送的图片

服务器端接收完成后反馈消息,客户端接收消息

  • 需要的流
    • 客户端
      • socket
      • 文件输入流(读图片)
      • 输出流(发送用)通过socket获取
      • 输入流(读服务器端反馈的消息)通过socket获得
    • 服务端
      • 客户端的socket
      • 输入流(读客户端的图片)通过socket获取
      • 文件输出流(保存用)
      • 输出流(发送反馈消息)通过Socket获取

注意:

  1. 异常需要用try-catch,这里偷懒

  2. 客户端的输出流和服务器端的输入流都是从Socket获取的

  3. 只有从本地读取 和保存是自己创建的

  4. 中断传输:socket.shutdownOutput();

//客户端
public void client1() throws IOException {
    //1.获取Ip对象
    InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
    //2.获取socket
    Socket socket = new Socket(inetAddress,6667);
    //4.获取输出流(用于发送图片)
    OutputStream os = socket.getOutputStream();
    //3.获取输入流(用于读取本地图片)
    FileInputStream fis = new FileInputStream("F:\\123.txt");
    //5.读写操作
    byte[] data = new byte[1024];
    int len ;
    while ((len = fis.read(data))!=-1){
        os.write(data,0,len);
    }
    //中断传输,表示自己已经传输完成,不再继续传输
    socket.shutdownOutput();
    //6.获取服务器端反馈的消息
    //6.1.获取输入流(读服务器端反馈的消息)
    InputStream is = socket.getInputStream();
    byte[] data2 = new byte[1024];
    int len2;
    while((len = is.read(data2))!=-1){
        String str = new String(data2,0,len);
        System.out.println(str);
    }
    //7.关闭流、socket
    is.close();
    os.close();
    fis.close();
    socket.close();
}
//服务器端
    @Test
public void server() throws IOException {
    //1.获取ServerSocket对象
    ServerSocket ss = new ServerSocket(6667);
    //2.获取客户端的socket
    Socket socket = ss.accept();
    //3.获取输入流(用于从客户端获取图片)
    InputStream is = socket.getInputStream();
    //4.获取输出流(保存本地)
    FileOutputStream fos = new FileOutputStream("F:\\321.txt");
    //5.读写操作
    byte[] data = new byte[1024];
    int len;
    while((len = is.read(data))!=-1){
        fos.write(data,0,len);
    }
    //6.服务器端发送反馈信息
    //6.1获取输出流
    OutputStream os = new socket.getOutputStream();
    os.write("收到文件,谢谢!".getBytes());
    //7.关闭流、socket、服务器
    os.close();
    fos.close();
    is.close();
    ss.close();
}

URL的实例化及常用方法

统一资源定位符

URL url = new URL("http://localhost:8080/examples/123.txt?username=tom")

getProtocol() 获取协议名
getHost() 获取主机名
getPort() 获取端口号
getPath() 获取文件路径
getFile() 获取文件名
getQuery() 获取查询名

使用tomcat复制文件

中断连接:urlConnection.disconnect();

public static void main(String[] args) throws IOException {
    //创建url对象
    URL url = new URL("http://localhost:8080/examples/123.txt");
    //获取 获取连接的对象
    HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
    //获取连接
    urlConnection.connect();
    //获取输入流(用于读连接中的数据)
    InputStream is = urlConnection.getInputStream();
    //获取输出流(用于存入硬盘)
    FileOutputStream fos = new FileOutputStream("F:\\1234.txt");
    byte[] data = new byte[1024];
    int len;
    while((len = is.read(data))!=-1){
        fos.write(data,0,len);
    }
    //关闭流,中断连接
    fos.close();
    is.close();
    urlConnection.disconnect();
}

文章作者: 小苏
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小苏 !
评论
  目录