JAVA高级阶段技术总结/知识点梳理/个人总结

String字符串

概念

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

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

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

//这句话执行时,创建一个"Tom"字符串对象,将其地址保存在变量name中

String name = “Tom”;

//这句话执行看似在改变字符串的值,实际是创建了一个新的"Jerry"字符串对象,将其地址保存到变量 name中

name = “Jerry”;

//以上两句,在内存中,会有两个字符串对象"Tom"和"Jerry",没有任何字符串发生了改变,只是****name引用 了不同的字符串地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wrEc5jA6-1670081493125)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== “点击并拖拽以移动”)]​编辑

String类使用时注意:

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

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

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

1使用**“”**赋值的形式创建

System.out.println(str1==str2);//true

System.out.println(str1==str3);//true

如图:

2**使用构造方法****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出来的地址保存到栈中变量str1中

String str2 = new String(“ab”);

​编辑3使用****+拼接""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

如图:

​编辑

总结:

0.上述创建字符串对象先判断堆中字符串常量池是否已有该字符,如有,不创建对象,没有创建对象,使用+****拼接""会创建StringBuilder对象然后调用StringBuilder的append方法,将"a"和new String(“b”)拼接,如:String str = “a” + new String(“b”);//一共创建了"a",“b”,String,StringBuilder这四个对象。

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

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

**3.**equals方法,该方法判断的原理大致为:将两个字符串用字符数组保存,逐个判断字符数组中的每个字符,全部一致 时返回true。注意比较的是字面值。在使用equals方法时,通常将已知的非空字符串作为调用者。

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

“admin”.equals(username);//这样写能避免空指针异常

字符串相关面试题

//题目一

String str1 = “ab”;//常量池中创建"ab"

String str2 = new String(“ab”);//堆中new String()保存常量池中已有的"ab"

String str3 = “a” + “b”;//用常量池已有的"ab"

String str4 = “a” + new String(“b”);//常量池中创建"a"和"b",堆中new String()和new

StringBuilder()

String str5 = “ab”;//用常量池已有的"ab"

System.out.println(str1 == str2);//false

System.out.println(str1 == str3);//true

System.out.println(str1 == str4);//false

System.out.println(str1 == str5);//true

//题目四

//这两句话执行后,会创建几个对象

String s5 = “hello”;//常量池:“hello”

String s6 = “hel” + new String(“lo”);//常量池:“hel"和"lo” 堆:new String()和new

StringBuilder

//5个对象:常量池:“hello”、“hel"和"lo”,堆:new String()和new StringBuilder

字符串String类中的常用方法

主要用于我们操作字符串

可变字符串

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

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

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

StringBuilder****类

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

StringBuffer****类

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

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

常用方法

作用

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() 反转字符串

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

String****转换为可变字符串

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

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

方法二:对象.toString();

String str=“hello”;

//通过构造方法将String"包装"为可变字符串对象

StringBuilder sb = new StringBuilder(str);

StringBuilder sb = new StringBuilder(“你好”);

//调用静态方法

String str = String.valueOf(sb);方法三:

可变字符串面试题

比较StringStringBuilderStringBuffer的区别

相同点:

这三个类都可以表示字符串。都提供了一些操作字符串的方法。

这三个类中有相同的方法,如charAt()、indexOf()等

这三个类都是被****final修饰的类,不能被继承

不同点:

String定义的字符串是一个常量。可变字符串定义的字符串是一个变量

String类中的方法,调用后,不会改变原本字符串的值;可变字符串类中的方法,调用后,会改变

原本字符串的值

StringBuilder是非线程安全的可变字符串类,StringBuffer是线程安全的可变字符串类,其中的方

法被synchronized修饰

总结

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

System****类

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

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

RunTime****类

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

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

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

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

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

方法调用时传值问题

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

Date****类

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

SimpleDateFormat****类

用于格式化日期的类。

Calendar****类

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

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

//获取Calendar类的对象

Calendar cal = Calendar.getInstance();

包装类

Java是纯面向对象语言,宗旨是将一切事物视为对象处理。

但原始类型不属于对象,不满足面向对象的思想。但原始类型在使用时无需创建对象,保存在栈中,效

率高。

为了让原始类型也有对应的类类型,达到"万物皆对象"的理念,所以就有了包装类的概念。

**包装类就是原始类型对应的类类型。**包装类通常用于字符串与原始类型之间的转换。

包装类

原始类型

Byte byte

Short short

Integer int

Long l ong

Float float

Double double

Character char

Boolean boolean

特点

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

Integer,char对应Character

包装类都是被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_VALUEMIN_VALUE这两个静态属性,用于获取对应类

型支持的最大最小值

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

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

如int类型

String num=“123”;

int i = Integer.parseInt(num);//123

其他同理

原始类型转换为字符串

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

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

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

装箱和拆箱

原始类型的数据转换为相应的包装类对象。这个过程称为装箱boxing

//手动装箱

int i = 123;//定义原始类型数据

Integer integer = Interger.valueOf(i);//调用包装类Integer的静态方法valueOf()将原

始类型转换为包装类对象

用于将包装类对象转换为原始类型。这个过程称为拆箱unboxing。

//手动拆箱

Integer integer = new Integer(123);//创建一个包装类对象

int i = integer.intValue();//调用包装类对象的intValue()方法将包装类对象转换为原始类

异常家族

处理异常

方式一:try-catch-finally语句

try{
//可能出现异常的代码
}catch(异常类 异常对象){
//如果出现异常对象,且与catch小括号中的异常类型匹配,就会执行这里的代码
}catch(异常类 异常对象){
//如果出现异常对象,且与catch小括号中的异常类型匹配,就会执行这里的代码
}finally{
//无论程序是否会抛出异常,都要执行的代码
}

final、finally、finalize的区别

final是一个修饰符,被final修饰的属性称为常量,方法不能被重写,类不能被继承

finally是try-catch-finally结构中的关键字,在无论是否抛出异常,都会执行的代码块

finalize是Object类中的方法,finalize()在某个对象被回收前调用的方法

方式二:throws关键字

这种方式,可以让编译时异常通过编译。

public class Test{

public void fun() throws InterruptException{//这时该方法就会有一个声明:该方法可能

会抛出异常

//这句话直接写完后,会报错,因为sleep()方法可能会抛出InterruptException异常,属于

编译时异常,必须要处理

Thread.sleep(500);

}

}

集合框架**(集合家族)**

ArrayListLinkedList****的区别

这两个类都是List接口的实现类,保存的元素有序可重复,允许保存null

ArrayList采用数组实现,随机读取效率高,插入删除效率低,适合用于查询

LinkedList采用双向链表实现,插入删除时不影响其他元素,效率高,随机读取效率低,适合用于

频繁更新集合

泛型

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

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

List list = new ArrayList();

//当前集合只能保存String类型的元素

list.add(“sdfsdf”);

//list.add(123);//无法添加

List list2 = new ArrayList();

list2.add(123);

哈希表****hash table

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

哈希码的特点

如果两个对象的hashCode不同,这两个对象一定不同

如果两个对象的hashCode相同,这两个对象不一定相同

hashCode相同,对象不同,这种现象称为哈希冲突

**“通话""重地”**这两个字符串的hashCode相同,但是两个不同的对象

HashSet****实现类

采用哈希表实现

元素不能重复,无序保存,允许保存一个null

本质是一个HashMap对象

使用HashSet集合时,通常要重写实体类中的equals和hashcode方法

HashSet****的应用

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

TreeSet****实现类

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

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

3只能添加同一种类型的对象且该类**实现了****Comparable接口(**必须要重写compareTo()方法

4compareTo()方法的返回值决定了能否添加新元素和新元素的位置

5添加的元素可以自动排序

TreeSet****的应用

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

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

Map****接口

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

文件类****File

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

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

//如想要表示 “F:\221001\笔记\面向对象部分回顾.pdf” 这个文件

//File(String pathName)

File file1 = new File(“F:\221001\笔记\面向对象部分回顾.pdf”);

//File(String parent,String child)

File file2 = new File(“F:\221001\笔记”, “面向对象部分回顾.pdf”);

//File(File parent,String child)

File parent = new File(“F:\221001\笔记”);

File file3 = new File(parent, “面向对象部分回顾.pdf”);

//file1、file2、file3都表示同一个文件

斐波那契数列
package com.hqyj.FileTest;
public class Test2 {
public static void main(String[] args) {
//兔子问题
//有一对兔子在第三个月开始,每个月都能生一对小兔子
//如果所有兔子不死亡,且每次生下的都是一雌一雄,问10个月后共有多少对兔子
//1月 2月 3月 4月 5月 6月 7月 8月 9月 10月
//1 1 2 3 5 8 13 21 34 55
//斐波那契数列
//f(n)=f(n-1)+f(n-2) n>2
Test2 t = new Test2();
System.out.println(t.f(20));
}
/** 递归方法* */
public int f(int n) {
if (n > 2) {
return f(n - 1) + f(n - 2);
}
return 1;
递归遍历文件夹
}
}

递归遍历文件夹

public class Test3 {
//查看某个目录下的所有文件
public static void main(String[] args) {
File source = new File("E:\\adobe");
Test3 t = new Test3();
t.fun(source);
}
/** 递归遍历文件夹* */
public void fun(File source) {
//输出某个目录中超过3个月未使用且大于500MB的文件
/*long start = source.lastModified();long end = System.currentTimeMillis();if ((end - start) / 1000 / 3600 / 24 > 90 && source.length() / 1024 /1024 > 500) {System.out.println(source.getName() + "\t" + newDate(source.lastModified()) + "\t" + source.length() / 1024 / 1024);}*/
//判断是否为目录
if (source.isDirectory()) {
//将其展开
for (File child : source.listFiles()) {
//因为子文件有可能是目录,继续调用本方法
fun(child);
}
}
 

}
}

流****Stream

流的分类

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

字节输入流****InputStream

字节输出流****OutputStream

字符输入流****Reader

字符输出流****Writer

流的四个父类的特点

这四个父类都是在java.io包下,都是抽象类,不能直接创建其对象,使用其子类创建对象

**FileInputStream文件字节输入流:**按字节读取硬盘中的文件。读取的文件必须存在

FileOutputStream文件字节输出流:按字节将内存中的数据写入到硬盘中。文件可以不存在但父目录必须存在。

FileInputStream****文件字节输入流

read()

读取一个字节,返回读取到的字节

read(byte[] bytes)

按字节数组读取,返回读取到的字节数量,读取到的内容保存在字节数组

FileOutputStream****文件字节输出流

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

过FileInputStream对象使用read(byte[] bytes)方法时,每次读取指定数组的字节,将读取到

的字节保存在字节数组中,该方法返回读取到的字节数量如果最后一次读取的字节数不足字节数

组的大小时,只会将读取到内容覆盖数组中最前的几个元素。所以会导致读取到的内容多于实际内

容。

在通过FileOutputStream对象使用write(byte[] bytes)方法时,会将字节数组中的所有内容写入到

输出流中,在最后一次写入时,可能会写入多余的内容。所以在写入时,最好使用write(byte[]

bytes,int off,int lef)方法,表示将字节数组中的内容,off开始写入len

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

FileInputStream fis = new FileInputStream(“d:/word.txt”);

FileOutputStream fos = new FileOutputStream(“d:/copy.txt”);

byte[] bytes = new byte[4];

//第一次读取4个字节,即aaab,count为4

int count=fis.read(bytes);

//写入数组中的全部内容

fos.write(bytes);

//第二次读取4个字节,即bbcc,count为4

count=fis.read(bytes);

//写入数组中的全部内容

fos.write(bytes);

//第三次读取1个字节c,覆盖数组中的第一个元素,即数组现在为cbcc,count为1

count=fis.read(bytes);

//写入数组中的全部内容

fos.write(bytes);//最终会写入aaabbbcccbcc

fos.write(bytes,0,count);//这样最后一次只会写入实际读取到的c

fos.close();

fis.close();

使用FileInputStream和****FileOutputStream 实现单文件的复制

import java.io.*;
public class CopyFile {
public static void main(String[] args) throws IOException {
//定义原文件和目标文件
File source = new File("F:\\221001\\录屏\\FileInputStreamFileOutputStream.mp4");
File target = new File("F:\\221001\\copy.mp4");
//定义文件字节输入流,用于读取原文件
FileInputStream fis = new FileInputStream(source);
//定义文件字节输出流,用于写入文件
FileOutputStream fos = new FileOutputStream(target);
/*
//调用无参的read()方法,表示读取一个字节,返回读取到的字节
int read = fis.read();
//如果能读取到内容
while (read > -1) {
//将读取到的内容写入到文件中
fos.write(read);
//继续读取
文件夹的复制
read = fis.read();
}
*/
//定义一个字节数组,大小为8MB
byte[] bytes = new byte[1024 * 1024 * 8];
//按字节数组读取,返回读取到的字节数量
int count = fis.read(bytes);
//循环读取写入
while (count > -1) {
//将读取的字节数组写入到文件中
// fos.write(bytes);//如果调用该方法,最后一次会多写入上一次残留的数据
fos.write(bytes,0,count);//如果调用该方法,实际读取到了多少字节就写入多少
count = fis.read(bytes);
}
fis.close();
fos.close();
if (target.exists()) {
System.out.println("复制成功");
}
}
}

BufferedReader****缓冲字符输入流

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

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

读取文本练习

import java.io.*;
public class Test2 {
public static void main(String[] args) throws IOException {
/*
File file = new File("F:\\221001\\笔记\\Java基础回顾.md");
//FileReader(File file)
Reader fr = new FileReader(file);
//BufferedReader(Reader in)
BufferedReader br = new BufferedReader(fr);
*/
//创建带有缓冲区的字符输入流对象
BufferedReader br = new BufferedReader(new FileReader("F:\\221001\\笔记
\\Java基础回顾.md"));
//循环判断是否还有字符
while (br.ready()) {
//读取整行
System.out.println(br.readLine());
}
//关闭最大的流对象即可
br.close();
}
}

BufferedWriter****缓冲字符输出流

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

写入文本练习

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.SimpleFormatter;
public class Test3 {
public static void main(String[] args) throws IOException {
File file = new File("221001.txt");
//创建缓冲字符输入流对象,读取文本
BufferedReader br = new BufferedReader(new FileReader(file));
//创建集合,保存读取到的姓名
ArrayList<String> list = new ArrayList<>();
//循环读取文件中的所有字符
while (br.ready()) {
String name = br.readLine();
list.add(name);
}
//关闭
br.close();
//打乱集合中的元素
Collections.shuffle(list);
//创建日期字符串
String today = new SimpleDateFormat("yyyy.MM.dd").format(new Date());
//创建缓冲字符输出流,用于写文本,文件名为"日期+作业情况.txt",如果每次都是新建,这样// BufferedWriter bw = new BufferedWriter(new FileWriter(today + "作业情.txt"));
//如果要追加,在new FileWriter("文件名",true)设置
BufferedWriter bw = new BufferedWriter(new FileWriter(today + "作业情
况.txt",true));
//写入字符串
bw.write("姓名\t\t是否完成");
//换行
bw.newLine();
Scanner sc = new Scanner(System.in);
//随机3个人
for (int i = 0; i < 3; i++) {
String name = list.get(i);
构造方法 说明
ObjectOutputStream(OutputStream
os)
创建一个对象字节输出流对象,参数为一个字节输出
流对象,由于OutputStream是抽象类,所以使用其
子类,如FileOutputStream对象,在其中定义要写入
的文件
常用方法 作用
writeObject(Object obj) 将一个对象写入到本地文件中
close() 关闭流对象
ObjectOutputStream对象字节输出流(序列
化)(掌握)
序列化:将对象转换为文件的过程
被序列化的对象,必须要实现Serializable接口。
这个接口是一个特殊的接口,没有定义任何方法,只是给该类加上标记,表示该类可以被序列化
构造方法
常用方法
ObjectInputStream对象字节输入流(反序列
化)(掌握)
反序列化:将文件转换为对象的过程
构造方法
System.out.println(name + "完成情况:");
String str = sc.next();
//写入读取到的内容
bw.write(name + "\t\t" + str);
//换行
bw.newLine();
}
bw.close();
}
}

ObjectOutputStream对象字节输出流(序列**)**

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

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

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

ObjectInputStream对象字节输入流(反序列**)**

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

网络编程

InetAddress****类

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

Socket类和ServerSocket****类

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

Socket指普通端

ServerSocket指服务器端

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

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
/** 使用套接字对象,实现客户端向服务端发送文件** 定义服务端套接字对象客户端* */
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类

  • 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);
        }
    }

}

线程的生命周期

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

守护线程

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

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

多线程访问同一个资源可能出现的问题

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

出现问题的原因

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

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

如何解决

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

synchronized关键字

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

修饰方法

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

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

修饰代码块

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

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

原理

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

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

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

多线程相关面试题

  • 实现多线程的方式

    • 继承Thread类
    • 实现Runnable接口后,包装为Thread对象
    • 匿名内部类
  • 为什么说StringBuilder或ArrayList、HashMap是非线程安全的

    package com.hqyj.ThreadSafe;
    
    public class Test {
        public static void main(String[] args) throws InterruptedException {
            // StringBuilder sb = new StringBuilder();
            StringBuffer sb = new StringBuffer();
            //循环10次创建10个线程对象
            for (int i = 0; i < 10; i++) {
                //创建线程对象
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        //每个线程都向StringBuilder对象中添加100次字符串
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        for (int j = 0; j < 10; j++) {
                            sb.append("hello");
                        }
                    }
                }).start();
            }
            Thread.sleep(5000);
            //如果正常,应该长度为10线程*10次添加*每次5个字母  长度为500
            System.out.println(sb.length());
            //如果用StringBuilder,最终的长度可能不为500
            //如果用StringBuffer,最终的长度一定为500
            //所有StringBuffer是线程安全的,适用于多线程
            //所有StringBuilder是非线程安全的,适用于单线程
        }
    }
    
  • 什么叫死锁?怎么产生?如何解决?

    如果有两个人吃西餐,必须有刀和叉,此时只有一副刀叉。

    如果A拿到了刀,B拿到了叉,互相都在等待另一个工具,但都不释放自己拥有的,这时就会造成僵持的局面,这个局面就称为死锁,既不结束,也不继续

  • 死锁的解决方式

    方式一

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

    如两个线程都先获取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,可以吃饭了");
                }
            }
        }
    }
    

总结: 多敲 多看 多问

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值