华清远见-重庆中心-JAVA高级阶段技术总结/知识点梳理/个人总结

String字符串

String是一个类,属于数据类型中的引用类型。

Java中一切使用""引起来的内容,都是这个类的实例,称为字符串对象。

字符串在定义后,值不可改变,是一个常量,实际是一个字符数组

//这句话执行时,创建一个"Tom"字符串对象,将其地址保存在变量name中
String name = "Tom";
//这句话执行看似在改变字符串的值,实际是创建了一个新的"Jerry"字符串对象,将其地址保存到变量name中
name = "Jerry";
//以上两句,在内存中,会有两个字符串对象"Tom"和"Jerry",没有任何字符串发生了改变,只是name引用了不同的字符串地址


//字符串可以当做数组使用
String str1 = "hello";

//字符串对象实际是一个字符数组对象"包装"而来
char[] list = {'h', 'e', 'l', 'l', 'o'};
String str2=new String(list);

System.out.println(str1);
System.out.println(str2);

String类使用时注意

如果频繁地将一个String类型变量的值进行更改时,会创建很多字符串对象。效率低,浪费内存空间。

所以在频繁更改字符串时,不要使用String类变量。

System.out.println("开始执行");
String str = "";
//5万次的循环,就会创建5万个字符串对象,但最终只会有一个字符串对象被str引用
for (int i = 0; i < 50000; i++) {
    str += i;
}
System.out.println("执行结束");

如果要频繁更改字符串,使用StringBuilder类或StringBuffer类

如何创建字符串对象

1.使用""赋值创建

String str="abc";

2.通过构造方法创建

常用构造方法说明
String()创建一个空白字符串对象。
String(String str)创建一个指定字符串的字符串对象。
String(char[] list)创建一个指定字符数组的字符串对象。
String(byte[] list,String charsetName)按指定的编码格式创建一个指定字节数组的字符串对象。

不同方式创建字符串的过程

使用""赋值的形式创建

//这句话执行时,先判断字符串常量池(缓冲区)中是否存在"ab",不存在则创建,将其地址保存到str1变量中
String str1 = "ab";
//这句话执行时,先判断字符串常量池(缓冲区)中是否存在"ab",已存在,不用创建,将其地址保存到str2变量中
String str2 = "ab";
//这句话执行时,+两端如果都是""定义的字符串,拼接后再判断字符串常量池(缓冲区)中是否存在
//拼接后的"ab"依然存在,将其地址保存到str3变量中
String str3 = "a" + "b";

//以上三句话,只会在内存中的字符串常量池(缓冲区)创建一个字符串对象"ab",分别引用给3个变量
System.out.println(str1==str2);//true
System.out.println(str1==str3);//true

可以使用Jdk中自带的反编译工具javap对class文件进行反编译

在class文件所在目录下(项目的Out目录中),进入控制台,输入javap -c 字节码文件名.class

使用构造方法String(String str)创建

//这句话执行时的流程
//1.在字符串常量池中寻找"ab",不存在,创建
//2.在堆中new String(),将字符串常量池中的"ab"保存到new出来的区域
//3.将堆中new出来的地址保存到栈中变量str1中
String str1 = new String("ab");

//这句话执行时的流程
//1.在字符串常量池中寻找"ab",存在,直接引用
//2.在堆中new String(),将字符串常量池中的"ab"保存到new出来的区域
//3.将堆中new出来的地址保存到栈中变量str2中
String str2 = new String("ab");

//由于str1和str2是堆中的两个区域,所以结果为false
System.out.println(str1 == str2);//false

使用+拼接""和new出来的字符串对象创建

//在字符串常量池中创建"ab"
String str1 = "ab";
//1.创建StringBuilder对象
//2.在字符串常量池中创建"a"
//3.在字符串常量池中创建"b"
//4.创建String对象
//5.调用StringBuilder的append方法,将"a"和new String("b")拼接
String str2 = "a" + new String("b");//一共创建了"a","b",String,StringBuilder这四个对象


//两个不同的地址
System.out.println(str1==str2);//false

字符串的比较

在使用字符串时,如果要比较其值是否相同,不要使用判断,因为判断的是内存地址。

所以在比较字符串是否相同时,要使用String类重写的equals方法进行判断。

该方法判断的原理大致为:将两个字符串用字符数组保存,逐个判断字符数组中的每个字符,全部一致时返回true,

所以比较的是字面值。在使用equals方法时,通常将已知的非空字符串作为调用者。

username.equals("admin");//这样写,username变量可能为空,会抛出空指针异常
"admin".equals(username);//这样写能避免空指针异常

字符串String类中的常用方法

方法名返回值作用
length()int得到字符串的长度
toLowerCase()String转换为小写
toUpperCase()String转换为大写
trim()String去除字符串首尾的所有空格
isEmpty()boolean判断字符串是否为空白字符串""
getBytes()byte[]将字符串转换为字节数组
toCharArray()char[]将字符串转换为字符数组
equalsIgnoreCase(String str)boolean忽略大小写判断两个字符串是否相同
equals(String str)boolean判断两个字符串是否相同
charAt(int index)char得到字符串指定索引上的字符
indexOf(String str)int得到字符串中某个子字符串第一次出现的索引,如果不存在,返回-1
lastIndexOf(String str)int得到字符串中某个子字符串最后一次出现的索引,如果不存在,返回-1
contains(字符序列)boolean判断某个子字符串是否在原字符串中出现
concat(String str)String将参数字符串拼接到原字符串末尾
startsWith(String str)boolean判断是否以指定字符串开头
endsWith(String str)boolean判断是否以指定字符串结尾
substring(int begin)String从指定索引开始截取字符串至末尾
substring(int being,int end)String截取[begin,end)区间内的字符串
split(String regex)String[]按执行字符串或正则表达式切分原字符串。如果指定内容不再末尾,n个指定字符能得到n+1个子串;如果指定内容在末尾,n个指定字符能得到n个子串(不包含末尾的无效字符)
replace(char oldChar,char newChar)String将原字符串中的所有指定字符替换为新字符
String.valueOf(参数)String将任意参数转换为字符串。通常用于原始类型转换为字符串。
String.formart(String 格式,Object… obj)String根据指定格式转换参数。常用于将浮点数保留小数。如String.format(“%4.2f”,10.0/3)表示将计算的结果四舍五入保留2位小数转换为字符串;如果最终数据所占位置小于4,原样输出,大于4在最前补充空格。

可变字符串

String字符串对象是一个常量,在定义后,值不可改变。

如果使用String类的对象,对其频繁更新时,就会不停地创建新的对象,不停引用给同一个变量。

如要执行10000次循环重新赋值的过程,就要创建10000个字符串对象,执行效率很低,这时就需要使用可变字符串对象。

package com.hqyj.StringBuilderTest;

/*
 * 可变字符串StringBuilder
 * */
public class Test1 {
    public static void main(String[] args) {

        System.out.println("程序开始执行");
        //System.currentTimeMillis();用于获取当前时间对应的毫秒数
        //从1970 1 1 0:0:0这一刻开始,到这句话执行时间隔的毫秒数
        long startTime = System.currentTimeMillis();
        /*
        //循环"更新"字符串,实际是在不停创建新的字符串
        String str = "";
        for (int i = 0; i < 50000; i++) {
            str += i;
        }*/
        //使用可变字符串StringBuilder对象,真正更新字符串
        //因为全程只有一个对象StringBuilder,每次循环只是在不停操作该对象,不会创建新对象,所以效率很高
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 500000; i++) {
            sb.append(i);
        }
        long endTime = System.currentTimeMillis();

        System.out.println("程序执行结束,用时" + (endTime - startTime) + "毫秒");

    }
}

StringBuffer类

用于表示可变字符串的一个类,是线程安全的,建议在多线程环境下使用。

StringBuilder和StringBuffer中的方法都一致,只不过StringBuffer中的方法使用了synchoronized关键字修饰,表示是一个同步方法,在多线程环境下不会出现问题。

StringBuilder类

用于表示可变字符串的一个类,是非线程安全的,建议在单线程环境下使用。

这里以StringBuilder为例

构造方法

常用构造方法作用
StringBuilder()创建一个大小为16的字符串数组,表示一个空白字符。类似于String str=“”;
StringBuilder(String str)创建一个str长度+16的字符数组后,将str添加到其中。类似于String str=“初始值”;

普通方法

常用方法作用
append(Object obj)将任意类型的参数添加到原可变字符串末尾
delete(int start,int end)删除[start,end)区间内的字符
deleteCharAt(int index)删除index索引上的字符
insert(int index,Object obj)在索引index上插入obj
replace(int start,int end,String str)将[start,end)区间内的字符替换为str
reverse()反转字符串

注意

  • 以上表格中的方法都是在直接操作同一个字符串对象,每次调用方法后,原字符串都会发生变化
  • StringBuffer和StringBuilder并没有重写equals方法,所以可变字符串的值是否相同时,调用的是equals中原始的==判断。如果要判断两个可变字符串的值是否相同时,需要将其转换为String后调用equals判断

可变字符串与String之间的转换

String转换为可变字符串

String str="hello";
//通过构造方法将String"包装"为可变字符串对象
StringBuilder sb = new StringBuilder(str);

可变字符串转换为String(任意类型对象转换为String)

方法一:String.valueOf(Object obj)方法

StringBuilder sb = new StringBuilder("你好");
//调用静态方法
String str = String.valueOf(sb);   

方法二:对象.toString();

StringBuilder sb = new StringBuilder("你好");
//调用toString()
String str = sb.toString();

方法三:

StringBuilder sb = new StringBuilder("你好");
//拼接一个空字符串
String str = sb + "";

总结

在频繁操作同一个字符串时,一定要使用可变字符串StringBuidler或StringBuffer类的对象,不能使用String类的对象。

System类

这个类中包含了一些系统相关的信息和一些方法。其中的属性和方法都是静态的。

该类不能创建对象,不是因为它是一个抽象类,而是因为它的构造方法是私有的。

常用属性和方法
System.out获取打印输出流PrintStream对象,用于控制台打印信息。
System.in获取输入流InputStream对象,用于获取输入的信息
System.err获取打印输出流PrintStream对象,用于控制台打印异常信息。
System.exit(int statues)终止虚拟机运行,参数0表示正常终止。
System.currentTimeMillis()获取从1970.1.1 0:0:0至今进过了多少毫秒。中国是UTC(+8),所以是从1970.1.1 8:0:0至今经过了多少毫秒。返回long类型。
System.arraycopy(原数组,原数组起始位置,目标数组,目标数组起始位置,原数组要复制的元素数量)将原数组中指定长度的元素复制到新数组中

RunTime类

Runtime类的对象,表示程序运行时对象(程序运行环境对象)。

包含了程序运行环境相关的信息。常用于获取运行环境信息(如虚拟机内存)或执行某个命令。

特点

这个类不是一个抽象类,但不能创建对象,因为它的构造方法是私有的。

这个类提供了一个静态方法getRuntime(),通过这个方法,可以获取一个Runtime类的对象。

这是Java中的一种设计模式–单例模式(一个类只能有一个创建对象)。

public class Runtime {
    //定义了私有的一个静态成员:当前类的对象
    //由于静态成员只在类加载时执行一次,所以这里只会创建唯一一个当前类的对象
    private static Runtime currentRuntime = new Runtime();

    //定义了一个公共的静态方法,用于获取创建的唯一的当前类的对象
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    //构造方法是私有的,不能在当前类之外创建对象
    private Runtime() {}
}

使用

package com.hqyj.test;

import java.io.IOException;

public class RuntimeTest {
    public static void main(String[] args) throws IOException, InterruptedException {
        //通过Runtime类的静态方法getRuntime()获取唯一的Runtime类的实例
        Runtime runtime = Runtime.getRuntime();

        System.out.println("当前虚拟机空闲内存" + runtime.freeMemory() / 1024 / 1024 + "MB");
        System.out.println("当前虚拟机实际最大内存" + runtime.totalMemory() / 1024 / 1024 + "MB");
        System.out.println("当前虚拟机支持的最大内存" + runtime.maxMemory() / 1024 / 1024 + "MB");

        //exec(String 指令名)运行某个指令,返回运行的进程对象
        //在指定秒后关机
        // Process process = runtime.exec("shutdown -s -t 300");
        //取消关机任务
        //  Process process = runtime.exec("shutdown -a");
        //mspaint画图 calc计算器 notepad记事本
        Process process = runtime.exec("mspaint");
        Thread.sleep(2000);   
        //通过进程对象调用销毁功能,从而关闭
        process.destroy();
    }
}

方法调用时传值问题

package com.hqyj.test2;

public class Test {
    /*
     *  当方法的参数为原始类型,方法中对该参数做修改,不会影响实际参数
     * */
    public static void fun1(int i) {
        i = 123;
        System.out.println(i);
    }

    /*
     *  当方法的参数为字符串时,方法中对字符串"重新赋值",实际是创建了一个新的字符串对象,不会影响实际参数
     * */
    public static void fun2(String str) {
        str = "new";
        System.out.println(str);
    }

    /*
     *  如果参数为引用类型,方法中直接操作该参数,操作的就是实际参数的内存地址,会影响实际参数
     * */
    public static void fun3(Person p) {
        p.setName("吴彦祖");
        System.out.println(p.getName());
    }

    /*
     * 如果参数为引用类型,方法中创建了一个新对象对其赋值,操作的是创建的新对象,不会影响实际参数
     * */
    public static void fun4(Person p) {
        p = new Person();
        p.setName("易烊千玺");
        System.out.println(p.getName());
    }

    /*
    * 如果参数为数组,也属于引用类型,方法中直接操作数组,操作的是实参数组,会影响实际参数
    * */
    public static void fun5(int[] list) {
        list[0] = 123;
        System.out.println(list[0]);
    }


    public static void fun(char[] list,Person p){
        list[0]='m';//这里在直接操作实际参数,会影响实参
        p = new Person();//这里创建了一个新的对象,操作的是方法中的对象,不会影响实参
        p.setName("刘鑫");
    }

    public static void main(String[] args) {
        //方法参数为原始类型,方法中对参数做修改,不会改变实际参数
        int i = 0;
        fun1(i);//123
        System.out.println(i);//0

        //方法参数为字符串,方法中对字符串重新赋值,不会改变实际参数
        String str = "old";
        fun2(str);//new
        System.out.println(str);//old

        //方法参数为引用类型,方法中对参数直接修改,会改变实际参数
        Person p = new Person();
        p.setName("王海");
        fun3(p);
        System.out.println(p.getName());

        //方法参数为引用类型,方法中创建新对象后赋值给实际参数,操作的是方法中的对象,不会改变实际参数
        Person p1 = new Person();
        p1.setName("赵敏");
        fun4(p1);
        System.out.println(p1.getName());

        //方法参数为数组,属于引用类型,方法中对参数直接修改,会改变实际参数
        int[] list = {0,1,2};
        fun5(list);
        System.out.println(list[0]);

        //练习
        char[] list2={'a','b','c'};
        Person p2 = new Person();
        fun(list2,p2);
        System.out.println(list2[0]);//m
        System.out.println(p2.getName());//null
    }
}

总结

参数只有是引用类型(类、数组、接口),并且方法中在直接操作该参数时,才会对实际参数造成影响。

fun3(Person p)参数为Person对象,方法中直接调用参数p的xxx方法,是在操作实际参数。

fun5(int[] list)参数为数组,方法中直接操作数组某个索引对应的元素,是在操作实际参数。

fun2(String str)和fun4(Person p)都在方法中创建了一个新的对象,是在操作方法中的参数,不影响实际参数。

public static void fun(char[] list,Person p){
    list[0]='m';//这里在直接操作实际参数,会影响实参
    p = new Person();//这里创建了一个新的对象,操作的是方法中的对象,不会影响实参
    p.setName("刘鑫");
}
public static void main(String[] args){
    char[] list={'a','b','c'};
    Person p = new Person();
    fun(list,p);
    System.out.println(list[0]);//方法内部直接操作数组,会影响实际参数,输出m
    System.out.println(p.getName());//方法内部创建了新对象,不会影响实际参数,输出null
}

Date类

用于表示日期时间的类,位于java.util包下

构造方法

常用构造方法说明
Date()创建当前瞬间对应的日期对象
Date(long l)创建指定瞬间对应的日期对象
Date(int year,int month,int day)该构造方法已过时。创建指定年月日的日期对象(年是1900年起经过的年数,月用0-11表示1到12月)

常用方法

常用方法作用
getTime()得到对应Date对象表示的毫秒数
setTime(long l)设置Date对象的毫秒数
after(Date when)判断调用日期对象是否在when之后
before(Date when)判断调用日期对象是否在when之前

SimpleDateFormat类

用于格式化日期的类。

构造方法

常用构造方法作用
SimpleDateFormat(String pattern);创建一个指定日期模板的格式化日期对象

日期模板

特殊字符作用
yyyy年份
MM月份
dd日期
HH小时
mm分钟
ss
E星期
以上两个字母都可以写成一个,如月份5M:5,MM:05
yyyy/MM/dd HH:mm:ss E2022/11/24 16:24:09 星期四

常用方法

常用方法返回值作用
format(Date date)String将Date对象按日期模板转换为字符串
parse(String str)Date将满足日期模板的字符串转换为Date对象

Calendar类

表示日历的类,包含了很多日历相关的信息。

是一个抽象类,无法创建对象。可以通过静态方法getInstance()获取该类的一个实例。

//获取Calendar类的对象
Calendar cal = Calendar.getInstance();

日历字段

在Calendar类中,定义了很多被final和static修饰的常量,称为日历字段,实际一个数字,用于获取指定信息

作用
Calendar.YEAR年份
Calendar.MONTH月份(0-11表示1-12月)
Calendar.DATE日期
Calendar.DAY_OF_WEEK星期(1-7表示周天到周六)
Calendar.HOUR12进制小时
Calendar.HOUR_OF_DAY24进制小时
Calendar.MINUTE分钟
Calendar.SECOND
Calendar.DAY_OF_MONTH本月第几天
Calendar.DAY_OF_YEAR本年第几天
Calendar.WEEK_OF_MONTH本月第几周
Calendar.WEEK_OF_YEAR本年第几周

常用方法

常用方法作用
get(int field)根据日历字段获取对应的值
getTime()获取对应的Date对象(Calendar对象转换为Date对象)
getMaximum(int field)获取指定日历字段支持的最大值,如Calendar.DATE最大31
getActualMaximum(int field)获取指定日历字段在当前日期下的实际最大值,如11月,Calendar.DATE最大30
set(int field,int value)将指定的日历字段设置为指定值
set(int year,int month,int date)同时设置日历对象的年月日
setTime(Date date)将Date对象作为参数设置日历对象的信息

包装类

Java是纯面向对象语言,宗旨是将一切事物视为对象处理。
但原始类型不属于对象,不满足面向对象的思想。但原始类型在使用时无需创建对象,保存在栈中,效率高。
为了让原始类型也有对应的类类型,达到"万物皆对象"的理念,所以就有了包装类的概念。
包装类就是原始类型对应的类类型。包装类通常用于字符串与原始类型之间的转换。
在web应用中,从浏览器页面中获取到后台的数据,全部都是String类型,所以一定要使用转换为原始
类型的方法。

包装类原始类型
Bytebyte
Shortshort
Integerint
Longlong
Floatfloat
Doubledouble
Characterchar
Booleanboolean

特点

  • 八个原始类型中,除了int和char,其余类型的包装类,都是将首字母改为大写。int对应Integer,char对Characte

  • 包装类都是被final修饰的,不能被继承

  • 除了Character类,其余包装类都有两个构造方法:参数为原始类型或String的构造方法。Character的构造方法只有一个,参数为char类型。这些构造方法用于将原始类型或字符串转换为包装类对象

  • 除了Character类,其余类都有静态方法parse原始类型(String str),用于将字符串转换为相应的原始类型

    • 数值型的包装类的parseXXX()方法,如果参数不是对应的数字,转换时就会抛出NumberFormat异常。如"123abc",或"123.4",在使用Integer.parseInt()时都会抛出异常

    • Boolean类型中的parseBoolean()方法,参数如果是"true"这四个字母,不区分大小写,都能
      转换为真正boolean类型的true,只要不是"true"这个单词,转换结果都为false

  • 除了Boolean类,其余包装类都有MAX_VALUE和MIN_VALUE这两个静态属性,用于获取对应类型支持的最大最小值

  • 所有包装类都重写了toString()方法,用于将包装类对象转换为String对象

字符串与原始类型之间的转换

字符串转换为原始类型

使用原始类型对应的包装类,调用parse原始类型(字符串)方法

原始类型转换为字符串

  • 使用+拼接一个空白字符串

  • 将原始类型转换为包装类后,调用toString()方法

  • String.valueOf(原始类型数据)

装箱和拆箱

  • 所有包装类都有一个静态方法valueOf(原始类型),将某个原始类型的数据转换为相应的包装类对象。这个过程称为装箱boxing。

  • 所有包装类都有一个原始类型Value()方法,用于将包装类对象转换为原始类型。这个过程称为拆箱unboxing。

  • 自动装箱和拆箱。在jdk1.5之后,为了方便原始类型和包装类之间做转换,加入了自动装箱拆箱的概念,可以直接将原始类型和包装类对象之间互相赋值

  • 自动装箱缓冲区

    • 如果通过构造方法创建的包装类对象,会有不同的内存地址,使用==判断结果为false

    • 自动装箱方式创建包装类对象,赋值范围在byte范围[-128,127]内,将这个值保存在缓冲区中,如果多个对象使用同一个数值,共享这个数据,使用同一个地址,使用判断结果为true;如果不再byte范围内,就会创建新的包装类对象,会有不同的内存地址,使用判断结果为false

    • 引用类型对象比较值是否相同时,不要使用==,而是要使用重写的equals方法

数组和集合

数组的特点

  • 数组中保存的元素都是有序的,可以通过下标快速访问
  • 数组中保存的数据都是同一种类型
  • 数组的长度在定义后,无法改变
  • 数组无法获取其中保存的元素实际数量

集合的特点

  • 能保存一组数据,可以有序可以无序
  • 集合的容量可变
  • 集合中可以保存不同类型的数据
  • 可以获取集合中保存的元素实际数量

集合框架(集合家族)

Collection还有父接口Iterable,但Iterable接口不算严格意义上的集合的根接口。它称为迭代器,是用于遍历集合元素的一个工具接口。

所以集合的根接口为Collection接口和Map接口,位于java.util包中。

Collection接口

该接口有两个核心子接口:List和Set。

这两个接口都可以保存一组元素,List接口保存元素时,是有序可重复的;Set接口保存元素时,是无序不重复的。

常用方法返回值作用
add(Object obj)boolean将元素添加到集合中
size()int获取集合中的元素数量
isEmpty()boolean判断集合是否为空
clear()void清空集合
contains(Object obj)boolean判断集合中是否存在指定元素
remove(Object obj)boolean移除集合中的指定元素
toArray()Object[]将集合转换为数组
iterator()Iterator获取集合的迭代器对象,用于遍历集合

List接口(有序可重复)

有序集合,元素可以重复,允许保存null,可以通过索引获取对应位置上的元素。

在该接口继承Collection接口的同时,又拓展了一些操作元素的方法,如添加到指定索引、根据索引删除、获取指定索引的元素、截取子集合的方法等。

常用方法返回值作用
get(int index)Object根据指定索引获取对应的元素
set(int index,Object obj)Object使用obj替换index上的元素,返回被替换的元素
add(int index,Object obj)void将obj添加到index上
remove(int index)Object移除指定索引的元素
indexOf(Object obj)int得到某元素第一次出现的索引,没有返回-1
lastIndexOf(Object obj)int得到某元素最后一次出现的索引,没有返回-1
subList(int from,int to)List截取[from,to)区间内的元素,返回子集合

ArrayList实现类(掌握)

  • 采用数组实现的集合
  • 可以通过索引访问元素,可以改变集合大小。如果要在其中插入或删除元素时,会影响后续元素
  • 该集合中保存的都是引用类型,即便保存了数组123,也保存的是Integer类型的123,而不是int类型的123
  • 该集合查询效率高,中途增加和删除元素效率低
构造方法
常用构造方法说明
ArrayList()创建一个Object类型的空数组。在调用添加方法后,才会更改该数组大小为10
ArrayList(int initialCapacity)创建一个指定容量的Object数组,如果参数为负,会抛出IllegalArgumentException异常
常用方法

ArrayList中的常用方法,就是Collection接口和List接口中定义的方法。

LinkedList实现类

  • 采用双向链表实现的集合
  • 集合中保存的每个元素也称为节点,除首尾节点外,其余节点都保存了自己的信息外,还保存了其前一个和后一个节点的地址
  • 如果在双向链表的数据结构中插入和删除操作节点时,不会影响其他节点的位置。如添加时新节点时,只需要重写定义新节点的前后节点位置即可
  • 如果要查询某个节点时,需要从头结点或尾结点开始一步步得到目标节点的位置
  • 双向链表在中间插入和删除的效率高,随机读取的效率低

构造方法

常用构造方法说明
LinkedList()创建一个空链表
常用方法

由于LinkedList既实现了List接口,又实现了Deque接口,所以还有Deque接口中的一些方法

实现Deque接口的方法说明
addFirst(Object obj)添加头元素
addLast(Object obj)添加尾元素
removeFirst()移除头元素
removeLast()移除尾元素
getFirst()得到头元素
getLast()得到尾元素
remove()移除头元素
pop()移除头元素
push(Object obj)添加头元素
peek()得到头元素
poll()移除头元素
offer(Object obj)添加尾元素

ArrayList和LinkedList的区别

  • 这两个类都是List接口的实现类,保存的元素有序可重复,允许保存null
  • ArrayList采用数组实现,随机读取效率高,插入删除效率低,适合用于查询
  • LinkedList采用双向链表实现,插入删除时不影响其他元素,效率高,随机读取效率低,适合用于频繁更新集合

Set接口(无序不重复)

无序集合,元素不可以重复,允许保存null,没有索引。

Set接口中没有自己定义的方法,都是继承于Collection接口中的方法

哈希表hash table

哈希表,也称为散列表,是一种数据结构,能更快地访问数据。

要保存的数据称为原始值,这个原始值通过一个函数得到一个新的数据,这个函数称为哈希函数,这个新数据称为哈希码,哈希码和原始值之间有一个映射关系,这个关系称为哈希映射,可以构造一张映射表,这个表称为哈希表。在哈希表中,可以通过哈希码快速地访问对应的原始值。

假设原本的数据为左侧的数组。

如果要查询10,需要遍历数组,效率不高。

通过一个特定的函数"原始值%5",得到一组新数据,让新数据重新对应元素,保存到“新数组”中,这个“新数组”称为哈希表。

这时如果要查询10,由于哈希函数是通过%5得到了0,所以直接查询哈希表中0对应的元素即可。

整个过程中,这个函数称为哈希函数,得到的新数据称为哈希码,新数组称为哈希表,对应关系称为哈希映射。

这个哈希函数,有一定的几率让多个原始值得到相同的哈希码,这种情况称为哈希冲突(哈希码一致,实际值不同),

为了解决哈希冲突,可以使用"拉链法",将2这个哈希码所在的位置向链表一样进行延伸。

哈希码的特点

  • 如果两个对象的hashCode不同,这两个对象一定不同
  • 如果两个对象的hashCode相同,这两个对象不一定相同
    • hashCode相同,对象不同,这种现象称为哈希冲突
    • **“通话""重地”**这两个字符串的hashCode相同,但是两个不同的对象

HashSet实现类

  • 采用哈希表实现
  • 元素不能重复,无序保存,允许保存一个null
  • 本质是一个HashMap对象
  • 使用HashSet集合时,通常要重写实体类中的equals和hashcode方法
构造方法
常用构造方法说明
HashSet()创建一个空集合,实际是创建一个HashMap对象。
常用方法

HashSet中没有属于自定义的方法,都是重写了父接口Set和Collection中的方法。这里参考Collection中的方法即可。

没有与索引相关的方法。

HashSet添加数据的原理

如果两个元素的hashCode相同且equals结果为true,视为同一个对象,不能添加。

每次向集合中添加元素时,先判断该元素的hashCode是否存在

  • 如果不存在,视为不同对象,直接添加
  • 如果存在,再判断equals方法的结果
    • 如果false,视为不同对象,可以添加
    • 如果true,视为同一对象,不能添加

由此可见,不能添加的条件是两个对象的hashCode相同且equals的结果为true。

如果每次只判断equals的话,由于equals方法通常重写时会判断很多属性,效率不高。

如果每次只判断hashCode的话,效率高,但有可能会有哈希冲突,

所以先判断hashCode,再判断equals,技能保证效率,又能保证不添加重复元素。

equals方法和hashCode的关系
  • 如果两个对象的equals方法结果为true,在没有重写equals方法的前提下,hashCode相同吗

    • 如果没有重写equals,默认是Object中使用==判断,如果结果为true,说明是同一个对象,hashCode一定相同
  • 如果两个对象的hashCode不同,在没有重写equals方法的前提下,equals方法的结果为?

    • hashCode不同,说明不是同一个对象,没有重写equals,说明使用Object中equals的==判断,结果为false
  • 如果两个对象的hashCode相同,equals方法的比较结果为?

    • 可能为true也可能为false

HashSet的应用

如果想要保存的对象保证不重复,且无关顺序,可以使用HashSet。如学生管理

TreeSet实现类

  • 特殊的Set实现类,数据可以有序保存,可以重复,不能添加null

  • 采用红黑树(自平衡二叉树)实现的集合

    • 二叉树表示某个节点最多有两个子节点
    • 某个节点右侧节点值都大于左侧节点值
    • 红黑树会经过不停的"变色"、"旋转"达到二叉树的平衡
  • 只能添加同一种类型的对象且该类实现了Comparable接口

    • 实现Comparable接口后必须要重写compareTo()方法
    • 每次调用添加add(Object obj)方法时,就会自动调用参数的compareTo()方法
  • compareTo()方法的返回值决定了能否添加新元素和新元素的位置

    • 如果返回0,视为每次添加的是同一个元素,不能重复添加
    • 如果返回正数,将新元素添加到现有元素之后
    • 如果返回负数,将新元素添加到现有元素之前
    • 添加的元素可以自动排实现类
    常用构造方法说明
    TreeSet()创建一个空集合,实际是创建了一个TreeMap对象 常用方法
    常用方法

    属于Set的实现类,所以能使用Collection和Set中的方法,除此之外,还有独有的方法

    常用方法作用
    fisrt()得到集合中的第一个元素
    last()得到集合中的最后一个元素
    ceil(Object obj)得到比指定元素obj大的元素中的最小元素
    floor(Object obj)得到比指定元素obj小的元素中的最大元素
    TreeSet的应用

    如果要保存的元素需要对其排序,使用该集合。

    保存在其中的元素必须要实现Comparable接口,且重写compareTo()方法,自定义排序规则

实现类
  • 特殊的Set实现类,数据可以有序保存,可以重复,不能添加null

  • 采用红黑树(自平衡二叉树)实现的集合

    • 二叉树表示某个节点最多有两个子节点
    • 某个节点右侧节点值都大于左侧节点值
    • 红黑树会经过不停的"变色"、"旋转"达到二叉树的平衡
  • 只能添加同一种类型的对象且该类实现了Comparable接口

    • 实现Comparable接口后必须要重写compareTo()方法
    • 每次调用添加add(Object obj)方法时,就会自动调用参数的compareTo()方法
  • compareTo()方法的返回值决定了能否添加新元素和新元素的位置

    • 如果返回0,视为每次添加的是同一个元素,不能重复添加
    • 如果返回正数,将新元素添加到现有元素之后
    • 如果返回负数,将新元素添加到现有元素之前
    • 添加的元素可以自动排实现类
    常用构造方法说明
    TreeSet()创建一个空集合,实际是创建了一个TreeMap对象
    常用方法

    属于Set的实现类,所以能使用Collection和Set中的方法,除此之外,还有独有的方法

    常用方法作用
    fisrt()得到集合中的第一个元素
    last()得到集合中的最后一个元素
    ceil(Object obj)得到比指定元素obj大的元素中的最小元素
    floor(Object obj)得到比指定元素obj小的元素中的最大元素
    TreeSet的应用

    如果要保存的元素需要对其排序,使用该集合。

    保存在其中的元素必须要实现Comparable接口,且重写compareTo()方法,自定义排序规则

Map接口

Map称为映射,数据以键值对的形式保存。保存的是键与值的对应关系。

键称为Key,值称为Value,键不能重复,键允许出现一个null作为键,值无限制。

键和值都是引用类型。

如,yyds就是一个键key,代表了一个含义:“永远单身”即为值value。

常用方法作用
size()得到键值对的数量
clear()清空所有键值对
put(Object key,Object value)向集合中添加一组键值对
get(Object key)在集合中根据键得到对应的值
remove(Object key)/remove(Object key,Object key)根据键或键值对移除
keyset()获取键的集合
values()获取值的集合
containsKey(Object key)判断是否存在某个键
containsValue(Object value)判断是否存在某个值
entrySet()得到键值对的集合

HashMap实现类(掌握)

  • JDK1.8之后,HashMap采用"数组+链表+红黑树"实现
    • 当没有哈希冲突时,元素保存到数组中
    • 如果出现哈希冲突,在对应的位置上创建链表,元素保存到链表中
    • 如果链表的长度大于8,将链表转换为红黑树
  • 数据采用键值对key-value的形式保存,键不能重复,能用null作为键;值没有限制,键和值都是引用类型
  • 向HashMap集合中添加元素时,原理同HashSet

构造方法

常用构造方法说明
HashMap()创建一个空的映射集合,默认大小为16,加载因子为0.75

常用方法

常用方法参考Map中的方法

遍历集合中元素的方式

遍历List集合

ArrayList<String> nameList = new ArrayList();
nameList.add("Tom");
nameList.add("Jerry");
nameList.add("LiHua");
nameList.add("Danny");

方式一:普通for循环

System.out.println("使用普通for循环遍历");
//方式一:普通for循环
for (int i = 0; i < nameList.size(); i++) {//从0遍历到size()
    String name = nameList.get(i);//通过get(int index)获取指定索引的元素
    System.out.println(name);
}

方式二:增强for循环

System.out.println("使用增强for循环遍历");
//方式二:增强for循环
for (String name : nameList) {
    System.out.println(name);
}

方式三:迭代器

System.out.println("使用迭代器遍历");
//方式三:迭代器
//Collection类型的集合对象.iterator(),获取迭代器
Iterator<String> iterator = nameList.iterator();
// iterator.hasNext()判断集合中是否还有下一个元素
// iterator.next();获取下一个元素
while (iterator.hasNext()) {
    String name = iterator.next();
    System.out.println(name);
}

遍历Set集合

Set hs = new HashSet();
hs.add(123);
hs.add("hello");
hs.add(null);
hs.add(987);

方式一:增强for循环

for(Object o : hs){
    System.out.println(o);
}

方式二:迭代器

Iterator<Object> it = hs.iterator();

while(it.hasNext()){
    System.out.println(it.next());
}

遍历Map集合

Map<Integer, User> hm = new HashMap<>();

User u1 = new User("admin", "123123");
User u2 = new User("tom", "123123");
User u3 = new User("jerry", "123123");
hm.put(1001, u1);
hm.put(1002, u2);
hm.put(1003, u3);

//遍历hashMap
for (Integer id : hm.keySet()) {//遍历键
    //根据键得到对应的值
    System.out.println(id + "\t" + hm.get(id).getUsername());
}

//得到当前hashmap对象中的所有键值对的集合
Set<Map.Entry<Integer, User>> entries = hm.entrySet();
//遍历键值对
for (Map.Entry<Integer, User> entry : entries) {
    System.out.println(entry);
}

泛型

一种规范,常用于限制集合中元素的类型,省去遍历元素时判断是否为对应类型和转型的过程

//集合在定义后,默认可以添加任意类型的数据,但通常情况下,都是保存同一种类型
List  list = new ArrayList();
list.add(123);
list.add(null);
list.add("hello");

//这时如果没有限制类型,使用增强for循环遍历集合中的元素时,就只能使用Object类型变量接收
for(Object o : list){

}

用法

在定义集合遍历时,在类后面写上**<引用数据类型>**

集合类或接口<引用数据类型> 集合变量名 = new 集合实现类();

List<String> list = new ArrayList();
//当前集合只能保存String类型的元素
list.add("sdfsdf");
//list.add(123);//无法添加

List<Integer> list2 = new ArrayList();
list2.add(123);

Collections集合工具类

  • Collection是集合的根接口,定义了集合操作元素的方法
  • Collections是集合的工具,定义了集合操作元素的静态方法

常用方法

常用方法说明
Collections.shuffle(List list)打乱List集合中元素的顺序
Collections.sort(List list)对List集合中的元素进行排序,元素必须实现Comparable接口
Collections.swap(List list,int a,int b)交换List集合中元素的索引
Collections.replaceAll(List list,Object oldObj,Object newObj)替换List集合中的旧元素为新元素
Collections.reverse(List list)将List集合中的元素反转
Collections.fill(List list , Object obj)使用指定元素填充List集合
Collections.rotate(List list , int n)将集合中最后n个元素放在最前
Collections.max(Collection col)/min(Collection col)得到集合中的最大/最小值,集合中的元素必须实现Comparable接口

集合和数组之间的转换

  • 集合转换为数组:使用Collection接口中的toArray()方法

    Object[] obj = 集合对象.toArray();
    
    List<Integer> list = new ArrayList();
    list.add(123);
    list.add(63);
    list.add(3);
    
    Integer[] nums =(Integer[]) list.toArray();
    
  • 数组转换为集合

    //一个数组对象
    int[] nums ={11,2,66,3,6,21};
    //定义集合对象
    List list = new ArrayList();
    //遍历数组的同时添加到集合中
    for(int i:nums){
        list.add(i);
    }
    
  • 一组数据转换为集合:使用Arrays工具类中的asList(一组数据)方法

    //通常将数组中的数据直接作为参数
    List<String> strings = Arrays.asList("XX", "aa", "qq", "xx");
    

文件类File

Java中的File类,表示本地硬盘中的文件(文件和目录)的一个类。

通过这个类创建的对象,可以操作对应的文件。

构造方法

常用构造方法说明
File(String pathName)根据文件的完整路径创建File对象
File(String parent,String child)根据文件的父目录路径和自身路径创建File对象
File(File parent,String child)根据文件的父目录对应的File对象和自身路径创建File对象

常用方法

常用方法说明
exists()判断文件是否存在
isFile()判断是否为文件
isDirectory()判断是否为目录
getName()获取文件名
getPath()获取文件相对路径
getAbsolutePath()获取文件绝对路径
getParent()获取父目录的名称
getParentFile()获取父目录对象
lastModified()获取最后一次修改时间对应的毫秒数
length()获取文件所占字节
isHidden()判断文件是否隐藏
delete()删除文件或空目录
renameTo(File newFile)将原文件重命名且移动到指定目录
mkdir()创建目录
list()获取某个目录下的第一层子文件的名称的数组
listFiles()获取某个目录下的第一层子文件对象的数组

IO

I:Input输入

O:Output输出

流Stream

在Java中,流用于表示计算机硬盘与内存之间传输数据的通道。

内存中的数据存入到硬盘中,称为写write,也称为输出Output

硬盘中的数据存入到内存中,称为读read,也称为输入Input

流的分类

Java中将流定义为类,以对象的形式表现流。流有"四大家族",是所有流的父类。

字节输入流InputStream

FileInpuStreamObjectInputStream

字节输出流OutputStream

FileOutputStreamObjectOutputStream

字符输入流Reader

FileReader、BufferedReader、OutputStreamWriter

字符输出流Writer

FileWriter、BufferedWriter、InputStreamReader

按方向分类

  • 输入流:InputStream、Reader

    • 将硬盘中的数据读取到内存中
  • 输出流:OutputStream、Writer

    • 将内存中的数据写入到硬盘中

按类型分

  • 字节流:InputStream、OutputStream
    • 读写非文本类型文件。如图片、音视频、其他文件等。
  • 字符流:Reader、Writer
    • 读写纯文本类型文件。如txt、md等

如要将硬盘中某个txt文件中的内容读取到程序中,使用Reader

如要将硬盘中的某个图片读取到程序中,使用InputStream

如要将程序中的文本写入到硬盘中为txt类型文件时,使用Writer

如要将程序中的数据写入到硬盘中为非文本文件时,使用OutputStream

流的四个父类的特点

  • 这四个父类都是在java.io包下,都是抽象类,不能直接创建其对象,使用其子类创建对象
  • 这四个父类中都定义了close()方法,用于关闭流对象,释放资源
  • 输入流(InputStream和Reader)都有read()方法读取数据到内存中,输出流都有write()方法写入数据到硬盘中
  • 输出流(OutputStream和Writer)都有flush()方法,用于将流中的数据冲刷到硬盘中
    • 在使用输出流对象时,一定要调用flush()或close()方法后,才能真正将数据写入到硬盘中
  • 所有的流中,以Stream结尾,都是字节流,数据以字节传输;以Reader或Writer结尾的,都是字符流,数据以字符传输
  • 读取硬盘中的数据,使用输入流,读取的文件必须存在;将数据写入到硬盘中,使用输出流,文件可以不存在,但父目录必须存在。
  • 读入或写入文本时,使用字符流;读取或写入非文本时,使用字节流

FileInputStream文件字节输入流(掌握)

按字节读取硬盘中的文件。

构造方法

常用构造方法说明
FileInputStream(String pathName)根据文件名创建流对象
FileInputStream(File file)根据文件对象创建流对象

常用方法

常用方法说明
read()读取一个字节,返回读取到的字节
read(byte[] bytes)按字节数组读取,返回读取到的字节数量,读取到的内容保存在字节数组中
close()关闭流对象

FileOutputStream文件字节输出流(掌握)

按字节将内存中的数据写入到硬盘中。

构造方法

常用构造方法说明
FileOutputStream(String pathname)根据文件名创建输出流对象,写入时覆盖原内容
FileOutputStream(String pathname,boolean append)根据文件名创建输出流对象,第二个参数为true,写入时追加在原内容之后
FileOutputStream(File file)根据文件对象创建输出流对象,写入时覆盖原内容
FileOutputStream(File file,boolean append)根据文件对象创建输出流对象,第二个参数为true,写入时追加在原内容之后

常用方法

常用方法作用
write(int i)写入一个指定字节
write(byte[] bytes)写入一个字节数组
write(byte[] bytes,int off,int len)写入字节数组中从off开始的len个字节
flush()将流中的数据冲刷到硬盘中
close()关闭流对象

使用FileInputStream和FileOutputStream读写时的注意事项

  • 在通过FileInputStream对象使用read(byte[] bytes)方法时,每次读取指定数组的字节,将读取到的字节保存在字节数组中,该方法返回读取到的字节数量。如果最后一次读取的字节数不足字节数组的大小时,只会将读取到内容覆盖数组中最前的几个元素。所以会导致读取到的内容多于实际内容。

  • 在通过FileOutputStream对象使用write(byte[] bytes)方法时,会将字节数组中的所有内容写入到输出流中,在最后一次写入时,可能会写入多余的内容。所以在写入时,最好使用write(byte[] bytes,int off,int lef)方法,表示将字节数组中的内容,从off开始写入len个。

    如有word.txt文件,其中保存aaabbbccc

FileReader文件字符输入流

按字符读取文件。

构造方法

常用构造方法说明
FileReader(String fileName)根据文件名创建文件字符输入流对象
FileReader(File file)根据文件对象创建文件字符输入流对象

常用方法

常用方法作用
ready()判断是否还有下一个字符
read()读取下一个字符,返回读取到的字符
read(char[] chars)按字符数组读取,返回读取到的字符数量,读取到的字符保存在字符数组中
close()关闭流对象

FileWriter文件字符输出流

按字符写入文件。

构造方法

常用构造方法作用
FileWriter(String fileName)按文件名创建字符输出流对象,覆盖写入
FileWriter(String fileName,boolean append)按文件名创建字符输出流对象,如果append为true,表示追加写入
FileWriter(File file)按文件对象创建字符输出流对象,覆盖写入
FileWriter(File file,boolean append)按文件对象创建字符输出流对象,如果append为true,表示追加写入

常用方法

常用方法作用
write(String str)按字符串写入
flush()将流中的数据冲刷到硬盘中的文件,必须调用该方法或close方法后,才能真正写入
close()关闭流对象

BufferedReader缓冲字符输入流(掌握)

自带缓冲区(字符数组)的字符输入流。默认字符数组大小为8192,每次最多读取8192个字符。

在读取纯文本文件(txt或md)时,首选该类。

构造方法

常用构造方法作用
BufferedReader(Reader in)创建一个带有缓冲区(大小为8192的char数组)的字符输入流对象,参数为Reader类型对象,Reader是抽象类,所以实际参数为Reader的子类,如FileReader,在FileReader对象中定义要读取的文件
BufferedReader(Reader in,int size)创建一个指定缓冲区(字符数组)大小的字符输入流对象

常用方法

常用方法作用
ready()判断是否还有字符
readLine()读取整行字符
close()关闭流对象

BufferedWriter缓冲字符输出流(掌握)

自带缓冲区(字符数组)的字符输出流

构造方法

常用构造方法说明
BufferedWriter(Writer writer)创建一个自带缓冲区的字符输出流对象,参数为一个Writer对象,Writer是一个抽象类,实际参数为Writer的子类,如FileWriter,在FileWriter中定义要将输入写入的目标文件
BufferedWriter(Writer writer,int size)创建一个指定缓冲区大小的字符输出流对象

常用方法

常用方法作用
write(String str)写入字符串
newLine()换行
flush()冲刷流中的数据到硬盘
close()关闭流对象

ObjectOutputStream对象字节输出流(序列化)(掌握)

序列化:将对象转换为文件的过程

被序列化的对象,必须要实现Serializable接口。

这个接口是一个特殊的接口,没有定义任何方法,只是给该类加上标记,表示该类可以被序列化

构造方法

构造方法说明
ObjectOutputStream(OutputStream os)创建一个对象字节输出流对象,参数为一个字节输出流对象,由于OutputStream是抽象类,所以使用其子类,如FileOutputStream对象,在其中定义要写入的文件

常用方法

常用方法作用
writeObject(Object obj)将一个对象写入到本地文件中
close()关闭流对象

ObjectInputStream对象字节输入流(反序列化)(掌握)

反序列化:将文件转换为对象的过程

构造方法

常用构造方法说明
ObjectInputStream(InputStream is)创建一个对象字节输入流对象,参数为一个字节输入流对象,由于InputStream是抽象类,所以使用其子类,如FileInputStream对象,在其中定义要读取的文件

常用方法

常用方法作用
readObject()读取序列化后的文件,返回类型为Object
close()关闭流对象

转换流

实际属于字符流,作用为将一个字节流对象转换为字符流对象

OutputStreamWriter

将字节输出流转换为字符输出流

InputStreamReader

将字节输入流转换为字符输入流

转换流的使用

如果只提供了一个字节流,但要向其中写入或读取字符时,就可以使用转换流将字节流转换为字符流。

使用字符流读写字符时比字节流更方便。

网络编程

InetAddress类

表示IP对象的一个类

public static void main(String[] args) throws UnknownHostException {
    //获取本机的ip对象
    // InetAddress ip = InetAddress.getLocalHost();
    //获取域名
    // System.out.println(ip.getHostName());
    //获取真实ip地址
    // System.out.println(ip.getHostAddress());

    //getByName(域名)  得到域名对应的ip对象
    //localhost域名表示本机,对应的ip地址为127.0.0.1
    InetAddress ip = InetAddress.getByName("localhost");
    //获取域名
    System.out.println(ip.getHostName());
    //获取ip地址
    System.out.println(ip.getHostAddress());
}

Socket类和ServerSocket类

都属于Socket(套接字)对象,表示网络中的某个端点

  • Socket指普通端
  • ServerSocket指服务器端

使用套接字对象实现两个端点(Socket和ServerSocket)之间发送文件

服务器端

package com.hqyj.uploadTest;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

/*
 * 使用套接字对象,实现客户端向服务端发送文件
 *
 * 定义服务端套接字对象
 * */
public class Server {
    public static void main(String[] args) throws IOException {
        //以本机创建服务端套接字对象
        ServerSocket server = new ServerSocket(8899, 100, InetAddress.getLocalHost());
        //等待客户端连接,返回连接的客户端套接字对象
        Socket client = server.accept();

        //定义要将读取到的数据写入到本地的文件字节输出流对象
        FileOutputStream fos = new FileOutputStream("上传文件.md");

        //获取客户端与服务端的输入流对象,读取发送的数据
        InputStream is = client.getInputStream();

        //定义读取的字节数组
        byte[] bytes = new byte[1024 * 1024 * 8];

        int count = is.read(bytes);
        while (count != -1) {
            //将读取到的数据写入到本地
            fos.write(bytes, 0, count);
            count = is.read(bytes);
        }

        fos.close();
        is.close();
    }
}

客户端

package com.hqyj.uploadTest;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

/*
 * 定义客户端套接字对象
 * */
public class Client {
    public static void main(String[] args) throws IOException {
        //创建客户端套接字对象,连接指定的服务端套接字对象
        Socket client = new Socket("192.168.31.39", 8899);
        //获取客户端与服务端的输出流对象
        OutputStream os = client.getOutputStream();

        //成功连接后,将某个文件发送给服务端
        //定义要发送的文件对象
        File file = new File("F:\\221001\\笔记\\面向对象部分回顾.md");

        //读取要发送的文件
        FileInputStream fis = new FileInputStream(file);

        //定义字节数组
        byte[] bytes = new byte[1024 * 1024 * 8];
        //循环读取要发送的文件
        int count = fis.read(bytes);
        while (count != -1) {
            //将读取到的数据写入到客户端套接字与服务端套接字的通道中
            os.write(bytes,0,count);
            count = fis.read(bytes);
        }

        fis.close();
        os.close();
    }
}

进程和线程

进程Process

进程就是操作系统中执行的程序。一个程序就是一个执行的进程实体。

每个运行中的进程,都有属于它独立的内存空间,各个进程互不影响。

线程Thread

线程是一个进程中的执行单元,一个进程中可以有多个线程。

多个线程,可以访问同一个进程中的资源。

每个线程都有一个独立的栈空间,这些线程所在的栈空间位于同一个进程空间中。

多线程

如果一个进程中,同时在执行着多个线程,就称为多线程。

多线程可以提高程序执行效率。如多个窗口卖票,可以加快卖票的效率。

其实每个执行的Java程序,都是多线程执行,main方法称为主线程,还有gc线程(守护线程)在同时运行。

如有一个工厂,工厂中有很多车间,每个车间有很多流水线。

工厂就是内存,车间就是各个进程,每个流水线都是一个进程中的一个线程。

并行和并发

并行

各个进程同时执行,称为并行。

并发

多个线程同时执行,称为并发。

同步和异步

同步

所有的任务排队执行,称为同步执行。

异步

在执行任务A的同时,执行任务B,称为异步执行。

Java中的线程Thread类

Java中,线程以对象的形式存在。

Thread类表示线程类

获取线程对象

  • 获取当前正在运行的线程对象

    Thread ct = Thread.cuurentThread();
    
  • 创建一个线程对象

    构造方法

    常用构造方法说明
    Thread()创建一个默认的线程对象
    Thread(String name)创建一个指定名称的线程对象
    Thread(Runnable target)将一个Runnable对象包装为线程对象
    Thread(Runnable target,String name)将一个Runnable对象包装为线程对象同时设置线程名

线程常用方法

方法作用
getId()获取线程id
getName()获取线程名,默认Thread-n
getPriority()获取线程优先级,默认为5
getState()获取线程状态
setName(String str)设置线程名
setPriority(int priority)设置线程优先级,范围在1-10,值越大越优先执行
isDaemon()判断线程是否为守护线程
setDaemon(boolean f)参数为true表示设置线程为守护线程
start()让线程进入就绪状态
run()线程获得执行权时执行的方法(线程要做的事情)
Thread.sleep(long m)设置当前线程休眠m毫秒
Thread.currentThread()获取当前执行的线程对象
Thread.yield()线程让步

实现多线程

方式一:继承Thread类

  • 1.创建一个类,继承Thread类
  • 2.重写Thread类中的run()方法
  • 3.创建自定义的线程子类对象后,调用start()方法

自定义Thread线程的子类

package com.hqyj.ThreadTest;

/*
 * 实现多线程步骤
 * 1.成为Thread的子类
 * 2.重写run()方法
 * 3.创建当前类对象后,调用start()方法
 * */
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //让该线程输出0-99
            System.out.println(getName() + ":" + i);
        }
    }

    public MyThread(String name) {
        super(name);
    }

    public MyThread() {
    }
}

main类

package com.hqyj.ThreadTest;

public class Test2 {
    public static void main(String[] args) {

        //创建无参数的自定义线程对象
        MyThread t1 = new MyThread();
        t1.setName("线程A");
        //创建自定义线程对象,参数为线程名
        MyThread t2 = new MyThread("线程B");

        //让两个线程自动执行,必须调用start()
        t1.start();
        t2.start();
    }
}

方式二:实现Runnable接口(建议使用)

由于Java中是单继承,如果某个类已经使用了extends关键字去继承了另一个类,这时就不能再通过extends继承Thread实现多线程。

就需要实现Runnable接口的方式实现多线程。

  • 1.自定义一个类,实现Runnable接口
  • 2.重写run()方法,将多线程要执行的内容写在该方法中
  • 3.创建Runnable接口的实现类对象
  • 4.使用构造方法Thread(Runnable target)或Thread(Runnable target,String name)将上一步创建的Runnable实现类对象包装为Thread对象

自定义Runnable接口的实现类

package com.hqyj.ThreadTest;
/*
 * 实现多线程步骤
 * 1.成为Runnable的实现类
 * 2.重写run()方法
 * 3.创建该类对象
 * 4.将其包装为Thread对象
 * */
public class MyThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //让该线程输出0-99
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

main类

package com.hqyj.ThreadTest;

public class Test2 {
    public static void main(String[] args) {

        //创建Runnable接口的实现类
        Runnable target = new MyThread2();
        //由于启动多线程必须要通过Thread的start()方法,所以一定要创建Thread对象
        Thread mt = new Thread(target,"线程A");//这里使用Thread(Runnable target)构造方法创建Thread对象
        //让线程就绪
        mt.start();
        //创建另一个线程对象,让线程就绪
        new Thread(new MyThread2(),"线程B").start();
    }
}

方式三:使用匿名内部类

如果不想创建一个Runnable接口的实现类,就可以使用匿名内部类充当Runnable接口的实现类

package com.hqyj.ThreadTest;

/*
 * 实现多线程的方式三:
 * 使用匿名内部类
 * */
public class Test3 {
    public static void main(String[] args) {


        //使用Thread(Runnable target ,String name)构造方法创建线程对象
        //此时new Runnable() { @Override public void run() {}}就是一个匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        }, "自定义线程").start();

        //如果main方法当做一个线程时,需要先启动其他线程后,在执行main方法中的内容,否则依然是按顺序执行
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }

}

线程的生命周期

线程的初始化到终止的整个过程,称为线程的生命周期。

img

新生状态

当线程对象被创建后,就进入了新生状态。

就绪状态

当某个线程对象调用了start()方法后,就进入了就绪状态。

在这个状态下,线程对象不会做任何事情,只在等他CPU调度。

运行状态

当某个线程对象得到CPU时间片(CPU执行这个线程的机会所给的时间),则进入运行状态,开始执行run()方法。

不会等待run()方法执行完毕,只会在指定的时间内尽可能地执行run()方法。只要调用玩run()方法后,就会再进入就绪状态。

阻塞状态

如果某个线程遇到了sleep()方法或wait()方法时,就会进入阻塞状态。

sleep()方法会在指定时间后,让线程重新就绪。

wait()方法只有在被调用notify()或notifyAll()方法唤醒后才能重新就绪。

终止状态

当某个线程的run()方法中的所有内容都执行完,就会进入终止状态,意味着该线程的使命已经完成。

守护线程

如果将一个线程设置setDeamon(true),表示该线程为守护线程。

守护线程会随着其他非守护线程终止而终止。

package com.hqyj.DaemonTest;
/*
* Test类是一个自定义线程类,死循环输出
* */
public class Test implements Runnable {
    public static void main(String[] args) {

        Thread thread = new Thread(new Test());
        //将自定义线程类设置为守护线程
        thread.setDaemon(true);
        thread.start();

        //main线程终止,守护线程也会终止
        for (int i = 0; i < 100; i++) {
            System.out.println("main方法中的循环执行中");
        }


    }

    @Override
    public void run() {
        while (true) {
            System.out.println("守护线程执行中。。。");
        }
    }
}

多线程访问同一个资源

可能出现的问题

如银行存款100,同一时刻在手机和ATM一起取出,如果用多线程模拟,可能会出现两个线程都取出100的情况。要避免这种情况发生。

本应该大于售出后再减,再打印剩余,由于线程A在打印"售出一张"后,还没来得及执行后续内容,其他线程就开始执行了。

出现问题的原因

由于线程调用start()方法后,就进入就绪状态。如果获得了CPU时间片,就开始调用run()方法,调用run()方法后,就会再次进入就绪状态,不会等待run()方法执行完毕,所以在线程A执行run()方法的时候,线程B也开始执行了,这样就会出现数据共享的问题。

因为现在所有的线程都是异步(同时)执行。

如何解决

让线程同步(排队)执行即可。这样一来,某个线程执行run()方法的时候,让其他线程等待run()方法的内容执行完毕。

synchronized关键字

这个关键字可以修饰方法或代码块

修饰方法

写在方法的返回值之前,这时该方法就称为同步方法。

public synchronized void fun(){
    //会排队执行的代码
}
修饰代码块

写在一个独立的{}前,这时该段内容称为同步代码块。

synchronized(要同步的对象或this){
    //会排队执行的代码
}
原理

每个对象默认都有一把"锁",当某个线程运行到被synchronized修饰的方法时,该对象就会拥有这把锁,在拥有锁的过程中,其他线程不能同时访问该方法,只有等待其结束后,才会释放这把锁。

使用synchronized修饰后的锁称为"悲观锁"。

方法被synchronized修饰后,称为同步方法,就会让原本多线程变成了单线程(异步变为同步)。

模拟死锁出现的情况

定义两个线程类,线程A先获取资源A后,在获取资源B;线程B先获取资源B后,再获取资源A。

如果对资源A和资源B使用了synchronized进行同步,就会在线程A获取资源A的时候,线程B无法获取资源A,相反线程B在获取资源B的时候,线程A无法获取资源B,所以两个线程都不会得到另一个资源。

PersonA线程

package com.hqyj.deadlock;

public class PersonA implements Runnable {
    //定义两个共享的成员变量,刀、叉
    private Object knife;
    private Object fork;


    public PersonA(Object knife, Object fork) {
        this.knife = knife;
        this.fork = fork;

    }

    /*
 * 该线程执行run方法时,先获取knife对象,等待3s后获取fork对象
 *
 * */
    @Override
    public void run() {

        synchronized (knife) {
            System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (fork) {
                System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
            }
        }
    }
}

PersonB线程

package com.hqyj.deadlock;

public class PersonB implements Runnable {
    //定义两个共享的成员变量,刀、叉
    private Object knife;
    private Object fork;


    public PersonB(Object knife, Object fork) {
        this.knife = knife;
        this.fork = fork;
    }

    /*
     * 该线程执行run方法时,先获取fork对象,等待3s后获取对象knife
     *
     * */
    @Override
    public void run() {

        synchronized (fork) {
            System.out.println(Thread.currentThread().getName() + "获取了fork,3s后获取knife");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (knife) {
                System.out.println(Thread.currentThread().getName() + "获取了knife,可以吃饭了");
            }
        }
    }

}

死锁的解决方式

方式一

让两个线程获取资源的顺序保持一致。

如两个线程都先获取knife,再获取fork

@Override
public void run() {
    synchronized (knife) {
        System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (fork) {
            System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
        }
    }
}

方式二

让两个线程在获取资源A和B之前,再获取第三个资源,对第三个资源使用synchronized进行同步,这样某个线程在获取第三个资源后,将后续内容执行完毕,其他线程才能开始执行。

如在获取knife和fork之前,先获取paper对象

@Override
public void run() {
    //先获取paper,再进行后续操作
    synchronized (paper) {
        synchronized (knife) {
            System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (fork) {
                System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
            }
        }
    }
}

总结

这个星期学了好多类,东西很多很杂。学起来相对于前面又更加的抽象。对于很多知识都是迷糊的,需要在用到的时候去翻阅笔记,降低写代码的速度。所以还是需要动手实操。对于很多类的区别,都还没有弄清楚。我就是老师说的那种,迷迷糊糊学,迷迷糊糊有结束了。
变量,刀、叉
private Object knife;
private Object fork;

public PersonB(Object knife, Object fork) {
    this.knife = knife;
    this.fork = fork;
}

/*
 * 该线程执行run方法时,先获取fork对象,等待3s后获取对象knife
 *
 * */
@Override
public void run() {

    synchronized (fork) {
        System.out.println(Thread.currentThread().getName() + "获取了fork,3s后获取knife");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (knife) {
            System.out.println(Thread.currentThread().getName() + "获取了knife,可以吃饭了");
        }
    }
}

}


## 死锁的解决方式

### 方式一

**让两个线程获取资源的顺序保持一致。**

如两个线程都先获取knife,再获取fork

```java
@Override
public void run() {
    synchronized (knife) {
        System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (fork) {
            System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
        }
    }
}

方式二

让两个线程在获取资源A和B之前,再获取第三个资源,对第三个资源使用synchronized进行同步,这样某个线程在获取第三个资源后,将后续内容执行完毕,其他线程才能开始执行。

如在获取knife和fork之前,先获取paper对象

@Override
public void run() {
    //先获取paper,再进行后续操作
    synchronized (paper) {
        synchronized (knife) {
            System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (fork) {
                System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
            }
        }
    }
}

总结

这个星期学了好多类,东西很多很杂。学起来相对于前面又更加的抽象。对于很多知识都是迷糊的,需要在用到的时候去翻阅笔记,降低写代码的速度。所以还是需要动手实操。对于很多类的区别,都还没有弄清楚。我就是老师说的那种,迷迷糊糊学,迷迷糊糊有结束了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值