JavaSE复习总结

Basic

Java基础内容
JDK的安装配置点这里

基础知识

标识符

标识符相当于Java中的名字,它有以下几个特点:

  1. 由字母、数字、下划线、美元符号组成;
  2. 严格区分大小写;
  3. 不能以数字开头;
  4. 不能使用关键字。

关键字

Java中共有50个关键字:
Java中的关键字其中goto和const是保留字,目前没有明确意思。

数据类型

基本数据类型

Java中共有8种基本数据类型:

  • 整数型:byte(占1个字节)、short(占2个字节)、int(占4个字节)、long(占8个字节);
  • 浮点型:float(占4个字节)、double(占8个字节);
  • 布尔型:boolean(占1个字节);
  • 字符型:char(占2个字节)。
引用数据类型
  • 我们对类创建对象时,Java会在堆内存中保存这个对象。
  • 引用数据类型的变量保存的就是这个对象的地址值。
类型转换

boolean类型不参与转换。

  • 隐式转换:小转大,直接转;
  • 显示转换:大转小,如:
	int i = 99byte b = (byte) i;

浮点数转成整型时,舍弃小数部分。

字面值规则

  • Java中,整数默认为int类型。
  • 小数默认为double类型。
  • byte、short、char三种比int小的类型,可以用使用范围内的值直接赋值。
  • 字面值后缀:L:long;F:float;D:double;
  • 字面值前缀:0b:二进制;0:八进制;0x:十六进制;\u:char类型十六进制。

运算符

  • 自增或自减:
	//前缀式,先使用i的值,再改变变量的值
	i++;
	//后缀式,先改变变量的值,再使用
	i--;
  • 三目运算符:
	//1是表达式,若1真,则2,否则3
	1? 2: 3

变量

成员变量
  • 成员变量有默认值,无需手动初始化。
  • 在整个类中都生效。
局部变量
  • 局部变量没有默认值,定义时需要初始化。
  • 在方法/代码块中生效,对应的方法或代码块执行完毕,局部变量也会随之释放。

重载

Overload:在一个类里有多个参数列表不同的同名方法。

流程控制

选择结构

拿变量a的值与case后的值比较,若相等,执行case后的操作,依次比较,如果遇到break,跳出switch语句,遇到default时,会执行default后的操作。
变量a支持byte、short、char、int、String类型的数据。

	//break可加可不加,看需求,default为保底选项
	switch(a){
		case 1 : 操作1; break;
		case 2 : 操作2; break;
		case 3 : 操作3; break;
		case 4 : 操作4; break;
		default : ...
	}

循环结构

while与do-while
  • while:先判断,再执行;
  • do-while:最少执行一次;先执行,再判断。
  • while循环设置死循环一定要写出口。
for

增强for循环:(主要用于遍历)

	for(类型 变量名: 需要遍历的变量){
	循环体
	}
	//例:
	String []str= {"aaaaa","96"};
	for(String s:str) {
		System.out.println(s);
	}

数组

  • 数组一旦创建后,长度是固定的,不可改;
  • 无论是在类里、方法里还是代码块中,数组的元素都是有默认值的;
  • 比如,在方法里创建一个int类型的变量,需要赋初值,但创建一个int类型的数组,可以不赋初值,默认值0(int类型);
  • 数组的下标从0开始。

创建

静态创建

创建时就赋值:

	int[] a = {1, 2, 3, 4};
	int[] a = new int[]{1, 2, 3, 4};
动态创建

必须说明数组的大小:

	int[] a = new int[5];
创建过程
  1. 在内存中开辟一块连续的空间,用来存放数据;
  2. 给完成数组初始化过程,给每个元素赋予默认值;
  3. 将数组的地址值赋给变量。

数组工具类Arrays

使用时需要导包。
常用方法:

  1. toString(数组名):除了char类型外(char类型底层做了处理),其他类型的数组想查看数组的具体元素,都要用这个方法,否则打印的是数组的地址值;
  2. sout(数组名):给数组排序,使用的是优化的快排算法;
  3. copyOf(要复制的数组, 新数组的长度):会重新创建一个数组,不会修改原数组。

OOP

万物皆对象。

三大特性

封装

封装就是把数据和基于数据的操作隐藏起来,保留一些对外公开接口,让使用者根据外部的接口操作。
封装的目的:

  1. 提高程序的安全性;
  2. 让资源按照我们预先规定的方式操作。

封装使用private关键字。

继承

  • 继承是Java最显著的特性,它允许创建分等级层次的类。
  • 继承的关键字为extends。
  • Java只支持单继承。
  • 继承具有传递性。
  • 子类拥有父类非private的属性和方法,但不能选择性继承,必须全部拥有。
  • 子类不能继承父类的构造方法(因为语法要求构造方法的名字是本类类名)。
  • 子类可以重写父类的方法。
  • 子类可以有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类创建对象时,会默认调用父类的构造方法,子类的构造函数中第一行默认存在"super();"。
  • 父类成员变量与子类成员变量同名时,用super.变量名指定父类的成员变量。
  • 继承提高了类之间的耦合性,耦合度高会造成代码之间的联系越紧密,代码独立性越差。
  • 被final修饰的类不能被继承。
Override

子类继承父类的方法后对父类的方法不满意时,可以重写父类的方法。
重写的要求:

  1. 子类方法的权限修饰符>=父类方法的权限修饰符;
  2. 方法名和参数列表相同;
  3. 子类方法的返回值类型与父类方法的返回值类型相同或者是父类方法返回值类型的子类。

可以给重写的方法上加@Override注解,标记这是一个重写的方法。

多态

多态指同一个实体有多种表现形式。
多态的前提:

  1. 继承
  2. 重写
  3. 父类引用指向子类对象:Parent p = new Child();
  • 父类的引用类型变量保存的是子类类型变量的地址值。
  • 当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。(即编译看左边,运行看右边)
  • 多态的出现是为了统一调用标准,向父类看齐提高了程序的可扩展性和可维护性。

资源的使用情况:

  1. 成员变量使用的是父类的;
  2. 成员方法使用的是父类声明,子类的实现;
  3. 如果父子类都有静态方法,使用的是父类的,静态方法不存在重写
  4. 静态成员: 随着类的加载而加载,谁调用就返回谁的

在Java中使用关键字class修饰,是对某一类事物的抽象。

构造方法

格式:与类名同名,没有返回值类型。
作用:创建对象。
分类:

  1. 无参构造:默认存在,如果添加了其他构造方法,无参构造会被覆盖;
  2. 含参构造;
  3. 全参构造:全参构造的参数要与本类属性一致。

变量

变量的使用原则:就近原则,即尽量控制变量的使用范围到最小。

  1. 局部变量:位置:方法里或局部代码块中;必须手动初始化来分配内存;
  2. 成员变量:位置:类里方法外;会被自动初始化赋予默认值。

构造代码块

  • 位置:类里方法外。
  • 作用:提取所有构造方法的共性功能。
  • 构造代码块优先于构造方法执行。

局部代码块

  • 位置:方法里。
  • 作用:控制变量的作用域。
  • 变量的作用范围越小越好,成员变量会存在线程安全的问题。

this

  • this关键字代表本类对象的一个引用对象 。
  • 普通的直接引用:当局部变量与成员变量重名时,使用this指定成员变量。
  • 引用构造函数:使用this在构造方法的第一行调用构造方法:
    1. this(); --调用的是本类的无参构造;
    2. this(参数); --调用的是本类的含参构造;
    3. 此时"this(参数列表); "必须写在构造方法的第一行。
  • 注意:禁止套娃,如下:
	//这是禁止的,会报错
	class A{
    int age;
    A(){
        this(1);
    }
    A(int age){
        this();
        this.age = age;
    }
}

super

  • super关键字用于指代本类的一个父类对象 。

  • 与 this 类似,super 相当于是指向当前对象的父类,这样就可以用 super.xxx 来引用父类的成员。

  • 引用构造函数:使用super在子类的构造方法的第一行调用父类构造方法:

    1. super(); --调用的是父类的无参构造;
    2. super(参数); --调用的是父类的含参构造;
    3. 调用super()必须写在子类构造方法的第一行。
  • 每个子类构造方法的第一条语句,都是隐含地调用 super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。

  • this() 和 super() 都指的是对象,所以,均不可以在 static 环境中使用。包括:static 变量,static 方法,static 语句块。

  • 从本质上讲,this 是一个指向本对象的指针, 然而 super 是一个 Java 关键字。

static

  • static可以修饰成员变量和方法。
  • 被static修饰的资源称为静态资源,静态资源随着类的加载而加载,优先于对象加载。
  • 静态资源可以被类名直接调用,也被称为类资源。
  • 静态资源存储在方法区中,被全局所有对象共享,值只有一份。
  • 静态资源只能调用静态资源。
  • 静态区域内不允许使用this和super关键字
静态代码块
  • 格式:static{}。
  • 位置:类里方法外。
  • 随着类的加载而加载,优先于对象加载,只加载一次,一般用于项目的初始化。
  • 顺序:静态代码块 - >构造代码块 ->构造方法 ->普通方法(如果普通方法里有局部代码块,执行局部代码块)
  • 如果有多个静态资源,加载顺序取决于位置

final

  • 被final修饰的类,不能被继承
  • 被final修饰的方法,不能被重写
  • 被final修饰的变量是个常量,值不能被改变

内部类

在 Java 中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。

  • 成员内部类:
    • 成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员);
    • 当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
      • 外部类.this.成员变量;
      • 外部类.this.成员方法。
    • 外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问。
  • 局部内部类:
    • 局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内;
    • 局部内部类就像是方法里面的一个局部变量一样,是不能有 public、protected、private 以及 static 修饰符的。
  • 匿名内部类:
    • 相当于创建了一个接口的实现类 + 重写接口中的所有方法
    • 匿名对象只能干一件事
  • 静态内部类:
    • 静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static;
    • 静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法。

对象

对象是对类的实例化

对象的创建过程

	class a = new A();
  1. 把A.class文件加载进内存
  2. 在栈内存中开辟一块空间,把引用类型变量a压入栈底,此时a有默认值;
  3. 在堆内存中开辟一块空间存放A类型的对象;
  4. 对成员变量初始化;
  5. 执行构造方法
  6. 把A类型的对象在对内存中的地址赋给a;

异常

  • 用来封装错误信息的对象
  • 顶级父类:Throwable
  • 子类:Error(系统错误,无法修复);Exception(程序可以修复的错误)
  • 异常的解决方案:
  1. 捕获处理try-catch --自己解决:
	//finally可写可不写
	try{
		可能出现异常的代码
	}catch(异常类型 异常的名字){
		万一捕获到了异常,进行处理的方案
	}finally{
		无论有没有捕获异常,都会执行的代码
	}
try-catch结构可以嵌套;
使用多态的思想,不论是什么子异常,都可以看作父类Exception做出更加通用的解决方案,甚至可以只写这一个。
  1. 向上抛出throws --交给别人解决:
//在会发生异常的方法上添加代码:throws 异常类型。例如:
public void method1()  throws Exception{}
public void method2()  throws ArithmeticException, InputMismatchException{}
如果一个方法抛出了异常,那么谁调用这个方法,谁就要处理这个异常,这里的处理也是这两种解决方案
不能把异常抛给main(),因为调用main()是JVM,没人解决了。

Abstract

  1. Java中可以定义被abstract关键字修饰的方法,这种方法只有声明,没有方法体,叫做抽象方法。
  2. 被abstract关键字修饰的类叫做抽象类。
  3. 如果一个类含有抽象方法,那么它一定是抽象类。
  4. 抽象类中可以没有抽象方法。
  5. 子类继承了抽象类以后,要么还是一个抽象类,要么就把所有抽象方法都重写。
  6. 抽象类不可以被实例化。
  7. 抽象类中可以有构造方法,父类的构造方法要优先于子类执行
  8. 抽象类中存在的构造方法不是为了创建本类对象时调用,而是为了创建子类对象时调用
  9. 抽象类中可以有变量、常量、普通方法
  10. 如果一个类中都是普通方法,那为什么声明称抽象方法? 原因:抽象类不可以创建对象
  11. 如果不想让外界创建本类对象,可以把普通类声明成抽象类

接口

Java里面由于不允许多重继承,所以如果要实现多个类的功能,则可以通过实现多个接口来实现,接口的关键字是interface。

接口与类相似点

  1. 一个接口可以有多个方法。
  2. 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
  3. 接口的字节码文件保存在 .class 结尾的文件中。
  4. 接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别

  1. 接口不能用于实例化对象。
  2. 接口没有构造方法。
  3. 接口不能包含成员变量,除了 static 和 final 变量。
  4. 接口不是被类继承了,而是要被类实现。
  5. 接口与接口之间支持多继承。

抽象类和接口的区别

  1. 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量会默认用 public static final 修饰。
  3. 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

接口的特点

  • JDK1.8以后接口中可以有被default修饰的默认方法或者被static修饰的静态方法(接口中的静态方法只能通过接口名调用
  • 接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
  • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
  • 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
  • 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
  • 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。
  • 一个类可以同时实现多个接口。
  • 一个类只能继承一个类,但是能实现多个接口。
  • 一个接口能继承另一个接口,这和类之间的继承比较相似。

设计模式

Java共有23中设计模式

单例设计模式

某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例。

饿汉式
  • 构造方法私有化:为了不让外界随意的实例化对象;
  • 对外提供一个公共的get()方法,返回本类对象给调用者,用static修饰此方法,后续可以通过类名直接调用。
懒汉式
  • 延迟加载:等到你需要的时候在帮你加载资源;
  • 程序中有共享资源single,并且有多条语句操作了共享资源,要加锁。

API

API(Application Programming Interface,应用程序接口)是一些预先定义的函数。目的是提供应用程序与开发人员基于某软件可以访问的一些功能集,但又无需访问源码或理解内部工作机制的细节。

基础API

Java.util包是java中的工具包,包含各种实用工具类/集合类/日期时间工具等各种常用工具包。

Object

  • Object类是所有Java类的祖先,也就是说我们所说的”顶级父类”,存在于java.lang.Object,这个包不需要我们手动导包;
  • 每个类都使用Object作为超类,所有对象(包括数组)都实现这个类的方法,在不明确给出超类的情况下,Java会自动把Object类作为要定义类的超类。

常用方法:

  • toString():用于返回对应对象的字符串表示;
  • hashCode():用于返回对应对象的哈希码值;
  • equals():用于指示其他某个对象是否与当前对象“相等”,比较的是两个对象的地址值。

String

  • String 类在底层是一个封装char[]数组的对象,并且用final修饰,所以不可改变,我们看到的修改其实是重新创建了一个String对象;
  • 创建方式:
    • String str = “xue”;(这种方式创建的字符串存放在公共池中)
    • String str = new String(“xue”);(这种方式创建的字符串存放在堆内存中)
  • 常用方法:
    • length():查看字符串的长度;
    • charAt():定位某个字符,返回它的位置;
    • lastIndexOf():某个字符最后一次出现的位置;
    • substring():截取子串,如果参数有两个左闭右开[1,5);
    • equals():判断两个串是否相等,注意String重写了Object的此方法,所以内容相同就返回true;
    • startsWith():判断是不是以参数开头;
    • endsWith():判断是不是以参数结尾;
    • split():以指定字符分割;
    • trim():去掉首尾两端的空格;
    • getBytes():把串转换成数组;
    • toUpperCase():变成全大写;
    • toLowerCase():变成全小写;
    • String.valueOf(10):把int类型的10转换成String类型。

StringBuffer/StringBuilder

  • 封装了char[]数组,和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象 ;
  • 提供了一组可以对字符内容修改的方法;
  • 常用append()来代替字符串做字符串连接”+”;
  • 内部字符数组默认初始容量是16:super(str.length() + 16);
  • 如果大于16会尝试将扩容,新数组大小原来的变成2倍+2,容量如果还不够,直接扩充到需要的容量大小。int newCapacity = value.length * 2 + 2;
  • StringBuffer 线程安全,StringBuilder在JDK1.5时提出,它相较于StringBuffer线程不安全,但速度更快,推荐使用。

正则表达式regex

正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个"规则字符串",这个"规则字符串"用来表达对字符串的一种过滤逻辑。
给定一个正则表达式和另一个字符串,我们可以达到如下的目的:

  • 给定的字符串是否符合正则表达式的过滤逻辑(称作"匹配");
  • 可以通过正则表达式,从字符串中获取我们想要的特定部分。
  • String提供了支持正则表达式的方法:
    • Matches(正则) : 当前字符串能否匹配正则表达式
    • replaceAll(正则,子串) : 替换子串
    • split(正则) : 拆分字符串

例如:

	String regex = "[0-9]{17}[0-9X]";
	//matches()用来判断字符串是否符合正则表达式的要求
	if(input.matches(regex)) {
		System.out.println("input right.");
	}else {
		System.out.println("input error.");
	}

正则表达式速查表:(图片来源
正则表达式速查表

自动装箱与自动拆箱

基本数据类型只存值,没有丰富的功能,我们把基本类型进行包装,提供更加完善的功能,如Integer、Double。

  • 自动装箱:把 基本类型 包装成对应的 包装类型 的过程:

Integer i = 5;

  • 编译器会自动把基本类型int 5包装成包装类型Integer然后交给Integer类型的引用类型变量i来保存
  • 编译器发生:Integer a = Integer.valueOf(5);
  • 自动拆箱:从 包装类型 的值,自动变成 基本类型 的值:

int a = i;

  • 编译器会自动把包装类型的 i 变会基本类型 int 然后交给int类型的变量 a 来保存
  • 编译器发生:int a = i.intValue();

BigDecimal

double是不精确的,如果要求精确的浮点数运算,可以使用BigDecimal。

  • 不要使用double作为参数的构造函数,double本身不精确,最好使用的是重载的参数类型是String的构造函数;
  • BigDecimal本身很精确,但有时候会除不尽,所以用bd1.divide(bd2)做除法在除不尽的情况下会报错
  • 除不尽的解决方案:bd1.divide(bd2, 10, BigDecimal.ROUND_HALF_UP),其中10是保留小数的位数,ROUND_HALF_UP是四舍五入

日期类

Date

IO

InputStream

InputStream是字节输入流的所有类的超类/抽象类,不可创建对象。
常用方法:

  • abstract int read():从输入流中读取数据的下一个字节;
  • int read(byte[] b):从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中;
  • int read(byte[] b, int off, int len):将输入流中最多 len 个数据字节读入 byte 数组,off表示存时的偏移量;
  • void close():关闭此输入流并释放与该流关联的所有系统资源。

普通子级:

  • FileInputStream:
    • 操作文件的字节输入流;
    • 构造方法参数:File file / String pathname。
  • BufferedInputStream:
    • 高效字节输入流;
    • 构造方法参数:InputStream,但无法创建抽象父类对象,所以一般传的是FileInputStream。

OutputStream

OutputStream是字节输入流的所有类的超类/抽象类,不可创建对象。
常用方法:

  • void flush():刷新此输出流并强制写出所有缓冲的输出字节;
  • abstract void write(int b):将指定的字节写入此输出流;
  • void write(byte[ ] b):将b.length个字节从指定的byte数组写入此输出流;
  • void write(byte[ ] b,int off ,int len):将指定byte数组中从偏移量off开始的len个字节写入输出流;
  • void close():关闭此输出流并释放与该流关联的所有系统资源。

普通子级:

  • FileOutputStream:
    • 操作文件的字节输入流;
    • 构造方法参数:File file / String pathname,默认存在一个参数boolean append,默认值为false,也就是覆盖输出,如果将FileOutputStream构造函数的第二个参数设为true,就会实现追加输出的效果。
  • BufferedOutputStream:
    • 高效字节输入流;
    • 构造方法参数:OutputStream,但无法创建抽象父类对象,所以一般传的是FileOutputStream。

Reader

Reader是字符输入流的所有类的超类/抽象类,不可创建对象。
常用方法:

  • int read():读取单个字符;
  • int read(char[] cbuf):将字符读入数组;
  • abstract int read(char[] cbuf, int off, int len):将字符读入数组的某一部分;
  • int read(CharBuffer target):试图将字符读入指定的字符缓冲区;
  • abstract void close():关闭该流并释放与之关联的所有资源。

普通子级:

  • FileIReader:
    • 操作文件的字符输入流;
    • 构造方法参数:File file / String pathname。
  • BufferedReader:
    • 高效字符输入流;
    • 构造方法参数:Reader,但无法创建抽象父类对象,所以一般传的是FileReader。

Writer

Writer是字符输入流的所有类的超类/抽象类,不可创建对象。
常用方法:

  • abstract void close():关闭此流,但要先刷新它;
  • void write(char[ ] cbuf):写入字符数组;
  • void write(int c):写入单个字符;
  • void write(String str):写入字符串;
  • void write(String str,int off,int len):写入字符串的某一部分;
  • abstract void write(char[] cbuf,int off,int len):写入字符数组的某一部分。

普通子级:

  • FileWriter:
    • 操作文件的字符输入流;
    • 构造方法参数:File file / String pathname,默认存在一个参数boolean append,默认值为false,也就是覆盖输出,如果将FileWriter构造函数的第二个参数设为true,就会实现追加输出的效果。
  • BufferedWriter:
    • 高效字符输入流;
    • 构造方法参数:Writer,但无法创建抽象父类对象,所以一般传的是FileWriter。

Serialization

序列化(Serialization)是将对象的状态信息转换为可以存储或传输形式的过程。
在序列化期间,对象将其当前状态写入到临时或持久性存储区,以后可以通过从存储区中读取或者反序列化对象的状态,重新创建该对象。

  • 序列化:利用ObjectOutputStream,把对象的信息按照固定的格式转成一串字节值输出并持久保存到磁盘;
  • 反序列化:利用ObjectInputStream,读取磁盘中之前序列化好的数据,重新恢复成对象。
  • 需要序列化的文件必须实现Serializable接口,用来启用序列化功能;
  • 不需要序列化的数据可以修饰成static,原因:static资源属于类资源,不随着对象被序列化输出;
  • 每一个被序列化的文件都有一个唯一的id,如果没有添加此id,编译器会自动根据类的定义信息计算产生一个;
  • 在反序列化时,如果和序列化的版本号不一致,无法完成反序列化;
  • Serializable接口中没有东西,就是标记一个类可以被序列化;
  • 常用与服务器之间的数据传输,序列化成文件,反序列化读取数据;
  • 常用使用套接字流在主机之间传递对象;
  • 不需要序列化的数据也可以被修饰成transient(临时的),只在程序运行期间在内存中存在,不会被序列化持久保存。

文件

创建一个文件对象并不是在磁盘中创建了一个文件。
常用方法:

  • long length():输出文件的字节大小;
  • boolean exists():判断文件是否存在;
  • String getName():获取文件名;
  • String getParent():获取父级目录;
  • String getAbsolutePath():获取绝对路径;
  • boolean mkdir():创建单级目录;
  • boolean mkdirs():创建多级目录;
  • boolean delete():删除文件或空的文件夹;
  • String[] list():查看文件夹下所有文件的名称;
  • File[] listFiles():列出文件夹中所有的文件夹和文件对象,数组的每个元素都是File对象,可进一步操作。

案例1:递归求指定目录总大小

需求:输入一个目录,返回该目录下所有文件的总字节大小

package cn.xue.file;

import java.io.File;
import java.util.Scanner;

/**
 * 本类用于递归求指定目录总大小
 * @Author YongJie Xue
 * @Date 2021/12/23 17:47
 */

public class SumSize_Recursion {
    public static void main(String[] args) {
        System.out.println("请输入您想计算的目录的路径:");
        String path = new Scanner(System.in).nextLine();
        File file = new File(path);
        long size = sumSize(file);
        System.out.println(file.getName().toString() + "的总大小为:" + size + "字节。");
    }

    private static long sumSize(File file) {
        File[] files = file.listFiles();
        long sum = 0;
        for(int i = 0; i < files.length; i++){
            if(files[i].isFile()){
                return sum + files[i].length();
            }else{
                return sum + sumSize(files[i]);
            }
        }
        return sum;
    }
}

案例2:复制文件

需求:将指定文件复制到指定位置

package cn.xue.file;

import java.io.*;
import java.util.Scanner;

/**
 * 本类用于复制指定文件到指定位置
 * @Author YongJie Xue
 * @Date 2021/12/23 18:07
 */

public class CopyFile {
    public static void main(String[] args) {
        System.out.println("请输入源文件的路径:");
        String sourcePath = new Scanner(System.in).nextLine();
        System.out.println("请输入目标文件的路径:");
        String targetPath = new Scanner(System.in).nextLine();
        File sourceFile = new File(sourcePath);
        File targetFile = new File(targetPath);
        copyFile(sourceFile, targetFile);
    }

    private static void copyFile(File sourceFile, File targetFile) {
        BufferedInputStream bufferedInputStream = null;
        BufferedOutputStream bufferedOutputStream = null;
        try {
            bufferedInputStream = new BufferedInputStream(new FileInputStream(sourceFile));
            bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(targetFile));
            int a = -1;
            while((a = bufferedInputStream.read()) != -1){
                bufferedOutputStream.write(a);
            }
            System.out.println("文件复制成功!");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("文件复制失败!");
        }finally{
            try {
                bufferedOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                bufferedInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

集合

集合框架图:(图片来源
集合框架图
常用方法:

  • boolean add(E e):将指定元素添加到集合中;
  • boolean isEmpty():判断集合是否为空;
  • void clear():清空集合;
  • int hashCode():返回本集合的哈希码值;
  • int size():返回本集合中元素的个数;
  • boolean equals(Object o):比较集合对象与参数对象o是否相等;
  • boolean contains(Object o):判断本集合是否包含指定的元素;
  • boolean remove(Object o):移除指定元素o;
  • Object[] toArray():将本集合转为数组;
  • boolean addAll(Collection<? extends E> c):将集合c中的所有元素添加到本集合中;
  • boolean containsAll(Collection<?> c):判断本集合是否包含集合c的所有元素;
  • boolean retainAll(Collection<?> c):取两个集合的公共元素,直接修改集合;
  • boolean removeAll(Collection<?> c):移除本集合中属于c的所有元素;
  • Iterator iterator():返回本集合的迭代器。

泛型

泛型是(Generics)JDK1.5 的一个新特性,其实就是一个『语法糖』,本质上就是编译器为了提供更好的可读性而提供的一种小手段,小技巧,虚拟机层面是不存在所谓『泛型』的概念的。

  • 泛型可以在接口、类、方法上使用:

    • public interface Collection< E >{}
    • public class A< T >{}
    • public < E > void method(E e){}:在方法的返回值前声明了一个< E >,表示后面出现的E是泛型,而不是普通的java变量
  • Java 中泛型标记符:

    • E - Element (在集合中使用,因为集合中存放的是元素)
    • T - Type(Java 类)
    • K - Key(键)
    • V - Value(值)
    • N - Number(数值类型)
    • ? - 表示不确定的 java 类型
  • 泛型中包容的不能是基本数据类型,必须是引用类型

  • <? extends T>表示该通配符所代表的类型是T类型的子类。
  • <? super T>表示该通配符所代表的类型是T类型的父类。

List接口

  • List集合是有下标的;
  • List集合是有顺序的;
  • List集合可以存放重复的数据;
  • 常用方法:
    • void add(int index, E element):在指定下标插入元素;
    • E get(int index):返回指定下标的元素;
    • E set(int index, E element):替换指定下标的元素;
    • int indexOf(Object o):返回指定元素在本列表中第一次出现的下标,如果不存在,返回-1;
    • int lastIndexOf(Object o):返回指定元素在本列表中最后一次出现的下标,如果不存在,返回-1;
    • List subList(int fromIndex, int toIndex):截取子列表[fromeIndex, toIndex);
    • ListIterator listIterator():返回此列表的迭代器。
ArrayList
  • 底层的数据结构是数组,内存空间是连续的;
  • 元素有下表,通常可以根据下标进行操作;
  • 增删操作比较慢,查询操作比较快(数据量大时)。
  • ArrayList相当于在没指定initialCapacity时就是会使用延迟分配对象数组空间,当第一次插入元素时才分配10(默认)个对象空间。假如有20个数据需要添加,那么会分别在第一次的时候,将ArrayList的容量变为10;之后扩容会按照1.5倍增长。
LinkedList
  • 底层的数据结构是链表,内存空间是不连续;
  • 元素有下表,但通常首尾节点操作比较多;
  • 增删操作比较快,查询操作比较慢(数据量大时)。
  • 常用方法:
    • public void addFirst(E e):添加首元素;
    • public void addLast(E e):添加尾元素;
    • public E getFirst():获取首元素;
    • public E getLast():获取尾元素;
    • public E removeFirst():删除首元素;
    • public E removeLast():删除尾元素;

Set接口

  • Set是一个不包含重复数据的Collection;
  • Set集合中的数据是无序的(因为Set集合没有下标);
  • Set集合中的元素不可以重复 – 常用来给数据去重;
  • HashSet : 底层是哈希表,包装了HashMap,相当于向HashSet中存入数据时,会把数据作为K,存入内部的HashMap中。
  • TreeSet : 底层是TreeMap,也是红黑树的形式,便于查找数据。

HashSet

  • HashSet 实现了 Set 接口
  • HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合;
  • HashSet 允许有 null 值;
  • HashSet 是无序的,即不会记录插入的顺序;
  • HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。

Map 接口

Java.util接口Map<K,V>,其中类型参数 : K - 表示此映射所维护的键 V – 表示此映射所维护的对应的值。Map常用于键值对结构的数据,其中键不能重复,值可以重复。
对Map集合迭代:

  • 把map集合中的K存到Set集合中;
  • 把map集合中一组K,V作为整体放入Set集合的一个Entry中。

HashMap

  • HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
  • HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。
  • HashMap 是无序的,即不会记录插入的顺序。
  • HashMap底层是一个Entry[ ]数组,当存放数据时,会根据hash算法来计算数据的存放位置:
    • 算法:hash(key)%n,n就是数组的长度,其实也就是集合的容量;
    • 当计算的位置没有数据的时候,会直接存放数据;
    • 当计算的位置,有数据时,会发生hash冲突/hash碰撞,解决的办法就是采用链表的结构,在对应的数据位置存放链表的头节点,对于这个链表来说,每次新加的节点会从头部位置开始加入,也就是说,数组中的永远是新节点。
  • HashMap的初始容量为16,加载因子为0.75,HashMap扩容会重新开辟空间,重新计算所有元素的位置,也叫rehash。

Other

线程

进程

程序:程序是数据与指令的集合,程序是静态的。
进程:进程就是正在运行的程序,进程是动态的。
进程的特点:

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的。
  • 并发性:多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响。

并行与并发:

  • 并行:多个CPU同时处理不同的进程;
  • 并发:多个进程抢占公共资源

线程

线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程,我们看到的进程的切换,切换的也是不同进程的主线程。
线程与进程的关系:

  • 一个操作系统中可以有多个进程,一个进程中可以有多个线程;
  • 每个线程有自己独立的内存,每个线程共享一个进程中的内存,每个线程又有自己独立的内存。

多线程运行时内存图:
内存图
线程的状态:

  • 新建状态:线程的创建需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中;
  • 就绪(可运行)状态:线程已经准备好运行,只要获得CPU,就可以立即执行。其实是将线程加入就绪队列中,等待OS选中;
  • 执行(运行)状态:线程已经获得CPU,其程序正在运行的状态。只有就绪状态才能切换成执行状态。
  • 阻塞状态:正在运行的线程由于某些事件(I/O请求、锁阻塞、休眠阻塞等)暂时无法执行的暂停状态,问题解决后再加入就绪队列中。
  • 终止状态:线程执行完毕或者因为异常退出run()方法,该线程结束生命周期,等待OS进行善后处理,释放资源,最后将PCB清零,并将PCB返回给系统。

线程状态的转换:
线程状态转换的代码对照线程的优先级:

  • 每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
  • Java 线程的优先级是一个整数,其取值范围是 :
    1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
  • 默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
  • 具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

多线程实现方案

继承Thread

Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。

  • 自定义一个类extends Thread;
  • 重写run()方法;
  • 创建线程对象;
  • 调用start()方法。

常用方法:

  • Thread():分配新的Thread对象;
  • Thread(String name):分配新的Thread对象,参数为线程的名字;
  • Thread(Runnable target):分配新的Thread对象,参数为Runnable实现类的对象;
  • Thread(Runnable target,String name):分配新的Thread对象;
  • public static Thread currentThread():返回对当前正在执行的线程对象的引用;
  • public long getId():返回该线程的标识;
  • public String getName():返回该线程的名称;
  • public void run():如果该线程使用独立的Runnable运行对象构造的,则调用该Runnable对象的run方法;
  • public static void sleep(long millisec):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行);
  • public void start():使该线程开始执行:Java虚拟机调用该线程的run();
  • public static void yield():暂停当前正在执行的线程对象,并执行其他线程;
  • public void interrupt():中断线程;
  • public final void setPriority(int priority):更改线程的优先级。
实现Runnable

如果自己的类已经extends另一个类,就无法多继承,此时,可以实现一个Runnable接口。

  • 自定义一个类implements Runnable;
  • 实现接口中的run()方法;
  • 创建目标业务对象target(接口实现类的对象);
  • 创建线程对象:Thread t = new Thread(target);
  • 通过线程对象调用start()方法,把线程对象加入就绪队列。

该方案相较于继承Thread的优点:

  • 耦合性不强,没有继承,后续仍然可以继承其他类;
  • 给所有线程对象统一业务,业务保持一致
  • 面向接口编程
线程池

详细讲解点这里
或者点这里
线程池是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

  • Executors:辅助创建线程池的工具类对象;
  • ExecutorService:线程池类;
  • public static ExecutorService newFixedThreadPool(int nThreads):创建一个最多nThreads个线程的线程池;
  • void execute(Runnable command):将一个线程加入就绪队列中。

在多线程编程中,会产生线程安全问题,即多个线程竞争同一资源。
线程安全出现的条件:在多线程程序中 + 有共享数据 + 多条语句操作共享数据。
同步与异步:

  • 同步:同一时刻只能有一个线程独占资源,其他线程排队,效率会降低,但保证了安全;
  • 异步:线程间互相不等待,互相抢占资源,有安全隐患,但效率会高一点。

同步锁:把有可能出现问题的代码包起来,一次只让一个线程执行。
详细描述点这里
通过sychronized关键字实现同步

  • 多线程间必须使用同一个锁
  • synchronized关键字可以用来修饰代码块,成为同步代码块,使用的锁对象类型任意但必须唯一;
  • synchronized关键字可以用来修饰方法,使用的锁对象是this。同步代码块同一个时刻只有一个线程进入,而同步方法可以让多个线程进入,所以使用本类代指对象this来确保同步。

其它锁的更多描述点这里

案例

需求:四个售票窗口卖100张票。
实现方法一:

package cn.xue.thread;

/**
 * 售票案例
 * @Author YongJie Xue
 * @Date 2021/12/24 14:12
 */

public class SaleTickets1 {
    public static void main(String[] args) {
        Sale sale = new Sale();
        Thread thread1 = new Thread(sale, "一");
        Thread thread2 = new Thread(sale, "二");
        Thread thread3 = new Thread(sale, "三");
        Thread thread4 = new Thread(sale, "四");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

class Sale implements Runnable{
    static int tickets = 100;
    @Override
    public void run() {
        while(true){
            if(tickets <= 0)break;
            synchronized (Sale.class){
                if(tickets > 0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("窗口" + Thread.currentThread().getName() + "卖了一张票,还剩" + --tickets + "张。");
                }
            }
        }
    }
}

实现方法二:

package cn.xue.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author YongJie Xue
 * @Date 2021/12/24 14:13
 */

public class SaleTickets2 {
    public static void main(String[] args) {
        Sale2 sale2 = new Sale2();
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for(int i = 0; i < 4; i++){
            executorService.execute(sale2);
        }
        //关闭线程池
        try {
            executorService.shutdown();
            if(!executorService.awaitTermination(10,TimeUnit.SECONDS)){
                System.out.println(" 到达指定时间,还有线程没执行完,不再等待,关闭线程池!");
                executorService.shutdownNow();
            }
        } catch (Throwable e) {
            executorService.shutdownNow();
            e.printStackTrace();
        }

    }
}
class Sale2 implements Runnable{
    static int tickets = 100;
    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    @Override
    public void run() {
        while(true){
            reentrantReadWriteLock.readLock().lock();
            if(tickets <= 0)break;
            reentrantReadWriteLock.readLock().unlock();
            if(tickets > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try{
                    reentrantReadWriteLock.writeLock().lock();
                    System.out.println("窗口" + Thread.currentThread().getName() + "卖了一张票,还剩" + --tickets + "张。");
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    reentrantReadWriteLock.writeLock().unlock();
                }

            }
        }

    }
}

注解

更详细点这里
注解可以增强java代码,同时利用反射技术可以扩充实现很多功能。它们被广泛应用于三大框架底层。
传统我们通过xml文本文件声明方式,而现在最主流的开发都是基于注解方式,代码量少,框架可以根据注解去自动生成很多代码,从而减少代码量,程序更易读。例如最火爆的SpringBoot就完全基于注解技术实现。

  • JDK自带注解:
    • @Override:用来标识重写方法;
    • @Deprecated:标记就表明这个方法已经过时了,但我就要用,别提示我过期;
    • @SuppressWarnings(“deprecation”):忽略警告;
    • @SafeVarargs:jdk1.7出现,堆污染,不常用;
    • @FunctionallInterface:jdk1.8出现,配合函数式编程拉姆达表达式,不常用。
  • 元注解:(定义其它注解的注解)
    • @Target:标记这个注解用在哪里:类上(ElementType.TYPE)、方法上(ElementType.METHOD)、属性上(ElementType.FIELD);
    • @Retention:标记这个注解的生命周期:源文件中(RetentionPolicy.SOURCE)、.class字节码文件中(RetentionPolicy.CLASS)、运行中(RetentionPolicy.RUNTIME);
    • @Inherited:允许子注解继承;
    • @Documented:生成javadoc时会包含注解,不常用;
    • @Repeatable:注解为可重复类型注解,可以在同一个地方多次使用,不常用。
  • 自定义注解:
    • 注解名定义的时候用"@interface 注解名"的方式来定义
    • 通过@Target注解表示此自定义注解可以使用的位置,使用时需要导包
      通过ElementType的静态常量值指定自定义注解使用的位置
      如果有多个值用{ ,}的格式来写:
      @Target({ElementType.METHOD, ElementType.TYPE})
      @Target底层维护的是一个ElementType数组
    • 通过@Retention注解表示此自定义注解的生命周期
      通过RetentionPolicy.静态常量来确定此自定义注解的生命周期,只能三选一
    • 自定义注解还可以添加功能 --给注解添加属性
      int age();不是方法的定义,而是自定义注解中定义属性age的语法
      如果为了使用方便,还可以给属性设置默认值,就可以直接使用
      格式:“int age() default 0;”
    • 自定义注解中还可以添加功能 --定义特殊属性value
      特殊属性的定义方式与普通属性的定义方式相同,只是使用方式不同
      使用此属性赋值的时候可以不用写成"@Test(value = “apple”)"
      格式可以简化成"@Test(“lemon”)",直接写值
    • 当自定义注解没有定义属性时,可以直接使用
      如果有属性,必须给属性赋值,或者在自定义注解定义属性时设置默认值
      如果给age赋值,要写成:@Test(age = 28) --要写完整
      如果给特殊属性赋值,可以简写成:@Test(“apple”)

例如:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)//表示此注解可以用来标记方法
@Retention(RetentionPolicy.SOURCE)
@interface Test{
        int age() default 0;
        String name() default "aaa";
        String value();
}
class TestAnno{
        String name;
        @Test("apple")
        public void eat() {
                System.out.println("干饭了");
        }
}

反射

Reflection(反射) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查,或者说“自审”,也有称作“自省”。
反射非常强大,它甚至能直接操作程序的私有属性。我们前面学习都有一个概念,private的只能类内部访问,外部是不行的,但这个规定被反射赤裸裸的打破了。
反射就像一面镜子,它可以在运行时获取一个类的所有信息,可以获取到任何定义的信息(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。

  • 获取字节码对象:
    • Class.forName(“类的全路径”);
    • 类名.class
    • 对象.getClass();
  • 获取包名、类名:
    • public Package getPackage():获取包,再使用getName()方法就可以获取包名;
    • public String getName():获取完整名字;
    • public String getSimpleName():获取简单名字。
  • 获取成员变量定义信息:
    • public Field[] getFields():获取所有公开的成员变量,包括继承变量;
    • public Field[] getDeclaredFields():获取本类定义的成员变量;
    • public Field getField(String name):获取名为name的属性;
    • public Field getDeclaredField(String name):获取名为name的私有属性。
  • 获取构造方法定义信息:
    • public Constructor<?>[] getConstructors():获取所有公开的构造方法;
    • public Constructor getConstructor(Class<?>… parameterTypes):获取公开的指定参数列表的构造方法;
    • public Constructor<?>[] getDeclaredConstructors():获取所有的构造方法,包括私有;
    • public Constructor getDeclaredConstructor(Class<?>… parameterTypes):获取私有的指定参数列表的构造方法。
  • 获取方法定义信息:
    • public Method[] getMethods():获取所有公开的方法,包括继承的方法;
    • public Method getMethod(String name, Class<?>… parameterTypes):获取公开的指定参数列表的方法;
    • public Method[] getDeclaredMethods():获取本类定义的方法,包括私有,不包括继承;
    • public Method getDeclaredMethod(String name, Class<?>… parameterTypes):获取私有的指定参数列表的方法。
  • 反射新建实例:
    • public T newInstance():执行无参构造创建对象;
    • public T newInstance(Object… initargs):执行含参构造创建对象,需要先获取构造方法。
  • 其它方法:
    • public void setAccessible(boolean flag):设置私有属性可以被访问或私有方法可以被调用;
    • public Object invoke(Object obj, Object… args):让指定实例来执行该方法。

字节码对象

在Java中可以将对象分为两大体系:字节码对象和实例对象。
类加载器(ClassLoader)在加载类时(将类读到内存)时都会创建一个字节码对象,且这个对象在JVM内存中是唯一的,此对象存储的是类的结构信息。
类加载时会做以下事情:

  • 构建类的字节码对象,类型为Class类型;
  • 可能会初始化类中的静态变量;
  • 了能会执行类中的静态代码块(由加载方式决定)。

字节码文件详解

Socket编程

Socket编程也叫套接字编程。套接字使用TCP提供两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。当连接建立时,服务器会创建一个 Socket 对象,客户端和服务器现在可以通过对 Socket 对象的写入和读取来进行通信。
java.net.Socket 类代表客户端和服务器用来互相沟通的套接字,并且 java.net.ServerSocket 类为服务器程序提供了一种来监听客户端,并与他们建立连接的机制。
ServerSocket:

  • public ServerSocket(int port):创建绑定到特定端口的服务器套接字;
  • public Socket accept():侦听并接受到此套接字的连接;

Socket:

  • public Socket(String host, int port):创建一个套接字,并将其连接到指定主机的指定端口号;
  • public InputStream getInputStream():获取此套接字的输入流;
  • public OutputStream getOutputStream():获取此套接字的输出流;
  • public void close():关闭此套接字。

案例

需求:客户端每输入一行数据,服务器给出一个回应。
服务器端:

package cn.xue.socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/**
 * 服务器端
 * @Author YongJie Xue
 * @Date 2021/12/24 20:42
 */

public class Server {
    public static void main(String[] args) {
        new Server().service();
    }

    private void service() {
        new Thread( ()-> {
            try {
                ServerSocket serverSocket = new ServerSocket(8888);
                System.out.println("服务器启动成功!");
                while(true){
                    Socket socket = serverSocket.accept();
                    System.out.println("客户端连接成功!");
                    Response response = new Response(socket);
                    response.start();
                }
            }catch(Exception e) {
                e.printStackTrace();
            }
        }).start();

    }
    private class Response extends Thread{
        Socket socket;
        Response(Socket socket){
            this.socket = socket;
        }
        Object object = new Object();
        @Override
        public void run() {
            BufferedReader in = null;
            BufferedWriter out = null;
            try {
                in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                String clientID;
                while(true){
                    synchronized (object) {
                        if((clientID = in.readLine()) != null) {
                            String data = in.readLine();
                            clientID = clientID.replaceAll("\n", "");
                            System.out.println("客户端" + clientID + "发来了:" + data + "\n请输入您的回应:");
                            String input = new Scanner(System.in).nextLine();
                            input = input + "\n";
                            Thread.currentThread().setName(String.valueOf(Thread.currentThread().getId()));
                            out.write(Thread.currentThread().getName() + "\n");
                            out.flush();
                            out.write(input);
                            out.flush();
                        }else break;
                    }
                }

            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客户端:

package cn.xue.socket;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * @Author YongJie Xue
 * @Date 2021/12/24 20:44
 */

public class Client {
    public static void main(String[] args) {
        try{
            Socket socket = new Socket("127.0.0.1", 8888);
            System.out.println("服务器连接成功!");
            BufferedWriter out = null;
            BufferedReader in = null;
            try{
                out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                while(true){
                    System.out.println("请输入您想发给服务器的数据:(输入0结束)");
                    String input = new Scanner(System.in).nextLine();
                    input = input + "\n";
                    if(!input.equals("0\n")){
                        String[] classNames = Thread.currentThread().getStackTrace()[1].getClassName().split("\\.");
                        out.write(classNames[classNames.length - 1] + "\n");
                        out.flush();
                        out.write(input);
                        out.flush();
                        String threadID = in.readLine();
                        String data = in.readLine();
                        System.out.println("您发给服务器:" + input + "线程" +threadID + "给您回了:" + data);
                    }else {
                        break;
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                in.close();
                out.close();
            }

        }catch(Exception e){
            e.printStackTrace();
        }
    }

}

可以创建多个客户端测试:(创建多个客户端类,改下类名即可)
此时会产生一个问题:假如有三个客户端,三个客户端连接之后在服务器还没有输入回复的时候都发来了数据,那么服务器会创建3个线程加入就绪队列中,此时在服务器输入回复,就无法指定是回复哪一个客户端了,因为线程是被OS随机选中的,这个问题留待以后解决。
运行测试码字不易,点个赞吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值