Java基础八股

80 阅读23分钟

JDK、JRE、JVM关系

JDK 包含 JRE,JRE 包含 JVM

JDK是Java的开发环境,包含了开发Java程序所需要的一切,例如编译器、JRE、库文件等

JRE是运行环境,它包含了用于执行Java程序所有必要组件,例如JVM、类库等

JVM是Java实现跨平台的核心,它负责在不同操作系统上执行Java字节码文件

面向对象三大特征

面向对象的三大特性是继承、多态和封装

继承:就是子类继承父类的属性和方法,让子类可以重用父类代码,并且可以添加自己的属性和方法

封装:就是将对象的属性和方法进行包装,隐藏实现细节,只对外暴露一个接口,Java通过访问修饰符(private、protected、public)实现封装

多态:就是同一个方法有多个不同的表现形式,Java中实现多态的三个必要条件是继承、重写和向上转型。多态分为编译时多态和运行时多态,方法重载是编译时多态,重写是运行时多态

面向对象五大原则

五大原则:单一职责、开放封闭、里氏替换、依赖倒置、接口隔离

单一职责原则:一个类最好只做一件事,有且仅有一个引起它变化的原因,这样有助于类代码的清晰、整洁

开放封闭原则:对修改关闭,对扩展开放,可以通过抽象类、接口和多态来实现

里氏替换原则:子类必须能够替换它的父类,并且可以在不破坏父类的前提下扩展方法和属性,确保继承关系的正确性和可靠性

依赖倒置原则:强调高层模块不依赖于底层模块,都应该依赖于抽象,以及抽象不依赖于具体,具体依赖于抽象,这样可以降低模块间的耦合性

接口隔离原则:强调不应该强迫程序使用它不需要的接口,推荐将大接口分解成多个专用接口,减少依赖关系

重载/重写

重写: 重写是运行时多态,子类继承父类然后对继承的方法进行修改,重写的方法名、参数列表和返回类型都必须相同或者是其子类型,子类方法的修饰符要大于等于父类,不能缩小访问权限

重载: 重载是编译时多态,一个类中有多个相同名称的方法名称但是有不同的参数列表,包括参数类型、数量等,返回值类型不能作为重载的标志,因为方法调用时编译器是根据调用参数来选择方法的

向上转型/向下转型

向上转型:是指将子类对象引用赋值给父类引用变量,向上转型后父类可以访问子类中属于父类的方法和属性,不能访问子类独有的属性和方法,作用是隐藏了子类型,提高了代码的扩展性

向下转型:并不是所有对象都可以向下转型,必须是先向上转型才能向下转型,转型前要先调用instanceof进行判断,以免出现ClassCastException

instanceof用法:
用它判断对象和某个类或接口的是否有继承关系,确保类型转换不会发生错误
//检查对象是否是特定类的实例
Object obj = new Integer(5);
boolean isInteger = obj instanceof Integer; // true
//检查对象是否是特定接口的实例
interface MyInterface {}
class MyClass implements MyInterface {}
MyInterface myObj = new MyClass();
boolean isInterfaceInstance = myObj instanceof MyInterface; // true
//检查对象是否是某个类的子类的实例
class Parent {}
class Child extends Parent {}
Parent parentObj = new Child();
boolean isChildInstance = parentObj instanceof Child; // true

JAVA基本数据类型

抽象类/接口

类或者类中有方法被abstract关键字修饰,这个类就是抽象类

抽象类可以包含普通的成员变量和默认方法实现,可以有构造器,但是不能被实例化,访问修饰符可以是public、protected和default,抽象方法必须被子类重写后才能被实例化,并且只能单继承

接口只能包括抽象方法和常量(final、static),没有构造器,所有方法默认都是public abstract,接口中的方法都需要被实现,可以实现多个接口

抽象类用来表示"is-a"的关系,用来共享通用方法

接口用来表示"has-a"的关系,用来定义类中应该具备的功能

Java修饰符

Java中有public、private、protected和default四种修饰符。public和default可以用来修饰类,它们四种都可以用来修饰方法和变量

public:对所有类可见

private:在同一个类中可见

protected:对用一个包中的类和所有子类可见

default:在同一个包中可见

Static关键字

static用来创建和类相关而不是与对象相关的成员,不需要创建对象就可以调用类的属性和方法,可以用在变量、方法和代码块

用在变量上被称为静态变量,一般用来存储类级别的共享数据,是Java实现共享内存的一种方法;用在方法上就是静态方法,静态方法不能访问非静态成员变量和方法,继承后不能重写,一般用在与类相关而不是与对象相关的方法上;用在代码块上就是静态代码块,只会在类加载时执行一次,一般用来做类级别的初始化操作

类初始化顺序

父类中的静态变量和静态代码块---子类中的静态变量和静态代码块---父类中的实例变量和普通代码块---父类的构造函数---子类的实例变量和普通代码块---子类的构造函数

Final关键字

final关键字可以用来修饰变量、方法和类,目的是为了防止修改和覆盖

被final修饰的类不可被继承;

被它修饰的方法不可被重写;

被它修饰的变量如果是基本类型,值不能再改变,如果是引用类型,变量就不能再引用其他对象了,但是对象本身是可以改变的

Final/finally/finalize

final关键字用来修饰类、方法和变量,强调的是不可继承、不可重写和不可变性

finally是try-catch-finally异常处理中的一环,一般用来做最后的资源释放,比如说管理连接池、释放锁

finalize是Object类的方法,在垃圾回收时如果对象重写了finalize()方法,会创建一个终结器引用记录它的地址,并且把终结器引用放到引用队列中,由优先级很低的finalize线程扫描调用,调用完成后如果对象仍然是不可达,就在下一次垃圾回收时进行回收

break/continue/return

break跳出当前循环,执行当前循环外的代码

contiune跳槽当前循环进行下一次循环

return是结束当前方法,并且携带返回值

String不变性

将String设计成不可变最重要的目的是为了做StringTable,利用字符串缓冲池避免存储多个相同字符串节约内存;另外不变性让String作为Map的key时Hash值不会产生变化,以及天然具备线程安全,可以在多线程中安全使用

String/StringBuffer/StringBuilder

String是只读的字符串对象,类由final修饰避免被继承,底层是一个由final修饰的字符数组,避免引用发生改变,对String对象的操作会创建一个StringBuilder对象,对它进行操作然后调用toString()生成一个新的String对象

StringBuffer和StringBuilder都继承了AbstractStringBuilder抽象类抽象类中维护的是可变的字符数组,所以在进行频繁字符串操作时通过会使用它们,如果要求线程安全就使用StringBuffer,它的方法都被synchronized修饰,通过互斥锁来实现线程安全

public class Main {
    public static void main(String[] args) {
        String a = "111";
        a = a + "1";
    }
}

反编译文件内容

equals/==

对于基础数据类型, == 比较的是值,对于引用类型, ==比较的是内存地址

equals()是Object类的方法,如果没有重写作用和==号相同,重写后一般比较的都是值。String就对equals()方法做了重写,它会先用==号判断是不是同一个对象,不然的话判断是否可以转换为字符串对象,之后再对字符串数组遍历比较判断内容是否相同。

public boolean equals(Object anObject) {
//地址是否相同
        if (this == anObject) {
            return true;
        }
//是否和String继承或实现了同样的类或接口
        if (anObject instanceof String) {
            //类型转换
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                //字符数组内容一一比较
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

Hashcode作用

Hashcode作用是减少equals()方法的调用,以及快速判断两个对象是否相等提高查询和插入效率。例如将对象插入到哈希表中,会先利用hashcode()找出桶位置,如果冲突再调用equals()方法对对象进行比较;在HashSet中通过对象的hashcode来判断是否是同一个对象

重写equals()后就需要重写hashcode()方法,避免同一个对象出现不同的hash值

以String举例,如果重写equals()方法不重写hashcode()方法,如果两个对象内容相同equals认为是同一个对象,hashcode仅仅根据地址,就出现同一个对象出现不同的hash值,所以String重写了hashcode()方法,用字符数组的内容来计算hash值

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

自动装箱/拆箱

装箱就是自动将基本数据类型转换为包装器类型,底层使用的是valueof() ;拆箱就是自动将包装器类型转换为基本数据类型,底层使用的是xxxValue()

对于Byte、Short、Integer、Long这四种包装类会默认创建 [-128, 127] 相应类型的缓存

== 比较两个包装类时,比较的是是否是同一个对象;如果有一个是基本类型就会自动拆箱然后比较值,如果是不同的包装类,会报不可比较的异常

当包装类调用equals()方法时,如果是两个包装类,会先判断是否可以进行转换,然后转换类型自动拆箱比较值

区别:

Java万物皆对象,而基本数据类型是非对象的,所以需要包装类将基本数据类型变成对象,使它们能够在面向对象的环境中使用,比如说包装类可以为null,可以用在泛型和集合

而基本数据类型不可以为null,不可以用在泛型和集合中

Integer a = 1;
Character b = '1';
a.equals(b);

 public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

异常/异常分类

Java中的异常分为Error和Exception两类,它们都继承自Throwable类

Error通常是严重的系统错误,一般是虚拟机或者底层系统错误引起,常见的有内存溢出(OutOfMemoryError)、栈溢出(StackOverflowError)等。

Exception通常由应用程序引起,分为检查型异常和非检查型异常,检查型异常在编译时就需要做处理,不然就会报错,这类异常通常是可预测的问题;非检查型异常又叫做运行时异常,程序运行时才会报错。

比如说调用sleep()方法需要显式的对中断异常做处理,而1/0是在程序运行时才会出错

throw和throws

throw用来在方法内部抛出异常,抛出的是一个具体的异常对象,检查型和非检查型都可以,一次只能抛出一个异常

throws用在方法声明上,用来指出方法可能抛出的异常类型,当发生异常时会将它传递给方法的调用者去做处理,它可以抛出多个异常

try-catch-finally

用来做异常处理,先try,遇到异常就执行catch,finally最后一定会被执行,即使发生异常。如果finally有return会返回finally中的return语句

另外对于catch,捕捉顺序从上到下应该是子类异常到父类异常,也就是从具体到普遍,否则会报异常,异常捕捉过程中会匹配最合适的异常类型,如果存在多个catch语句生效一个后会直接退出catch语句

class A extends Exception{
    A(){
        System.out.println("A");
    }
}

class B extends A{
    B(){
        System.out.println("B");
    }
}

public class Main {
    public static void main(String[] args) throws A {
        try {
            1.throw new A();
            2 throw new B();
        }catch (B b) {
            System.out.println("bbb");
            throw new A();
        }catch (A a) {
            System.out.println("aaa");
        }finally {
            System.out.println("last");
        }
    }
}
对于1,抛出异常对象A,从上到下开始匹配catch异常类型,A异常类型最匹配
A
aaa
last

对于2,抛出异常对象B,先调用父类构造函数再调用子类构造函数,
然后输出"bbb",抛出异常对象A,但是catch语句已经捕获了一个异常,
所以需要借助throws在方法声明上向外抛出,但是外层没有处理导致异常,
在异常发生之前先执行finally语句块中的内容
A
B
bbb
A
last
Exception in thread "main" x.A
	at x.Main.main(Main.java:23)

Object类常用方法

Object中常用方法有clone、equals/hashcode、toString、wait/notify/notifyAll、getClass方法。

clone():它由protected修饰,用于实现对象的浅拷贝,对象类只有实现Cloneable接口才能调用它

equals()/hashcode():equals用来比较两个对象是否相等,不重写和==号作用相同;hashcode用来减少equals方法的调用次数和快速比较两个对象是否相同

toString():用来做对象的输出,一般要进行重写,不然只是输出对象的地址

wait()/notify()/notifyAll():wait用来让线程进行等待或者是计时等待状态,notify/notifyAll用来唤醒一个或者是全部线程

getClass():用来获取对象的Class对象

深拷贝和浅拷贝

浅拷贝对基本类型做值传递,对引用类型数据只是复制一个引用指向原始引用的对象,类似于快捷方式,实现它的方式有实现Cloneable接口、构造函数

深拷贝对基本数据类型做值传递,对引用数据类型会创建一个新对象,然后复制它的内容,引用指向了新的对象,相当于开分店,实现它的方式有构造函数、序列化或者是第三方库如Gson

实现浅拷贝
1.通过Cloneable接口实现浅拷贝
public class ShallowCopy1 implements Cloneable {
    public Integer number;
    public String text = new String("111");

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) 
    throws CloneNotSupportedException {
        ShallowCopy1 copy = new ShallowCopy1();
        ShallowCopy1 clone = (ShallowCopy1) copy.clone();
        System.out.println(clone.text == copy.text);
    }
}
输出:true

2.通过构造函数实现浅拷贝
public class ShallowCopy2 {
    public String city;

    ShallowCopy2(String city) {
        this.city = city;
    }
    ShallowCopy2(ShallowCopy2 shallowCopy2) {
        this.city = shallowCopy2.city;
    }

    public static void main(String[] args) {
        ShallowCopy2 copy1 = new ShallowCopy2("北京");
        ShallowCopy2 copy2 = new ShallowCopy2(copy1);
        System.out.println(copy1.city == copy2.city);
    }
}
输出:true

实现深拷贝
1.构造函数实现深拷贝
class A{
    public String city;
    public A(String city) {
        this.city = city;
    }
    public A(A a) {
        this.city = new String(a.city);
    }
}
public class DeepCopy1 {
    public static void main(String[] args) {
        A copy1 = new A("北京");
        A copy2 = new A(copy1);
        System.out.println(copy1.city == copy2.city);
    }
}
输出:false
2.序列化实现深拷贝
class B implements Serializable{
    public String city;
    public B(String city) {
        this.city = city;
    }
    public B deepCopy() throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = 
                new ByteArrayOutputStream();
        ObjectOutputStream out = 
                new ObjectOutputStream(bos);
        //将对象写入到bos中 - 序列化
        out.writeObject(this);

        ByteArrayInputStream bis =
                new ByteArrayInputStream(bos.toByteArray());
        //将bos读入到bis中,然后再写到in中
        ObjectInputStream in = 
                new ObjectInputStream(bis);
        //反序列化
        return (B) in.readObject();
    }

}
public class DeepCopy2 {
    public static void main(String[] args) 
            throws IOException, ClassNotFoundException {
        B copy1 = new B("北京");
        B copy2 = copy1.deepCopy();
        System.out.println(copy1.city == copy2.city);
    }
}
输出:false

3.使用第三方库如Gson完成深拷贝

值传递/引用传递

值传递:指的是调用函数时,将实际参数复制一份传递到函数中去,函数对副本进行操作

引用传递:指的是直接将实际参数的地址传递到函数中去,函数对本体进行修改

Java只有值传递,引用类型实际上是拷贝了一份地址,拷贝的地址和真实地址都指向同一个对象

Java泛型

泛型就是将类型参数化,只有在编译时才能确定具体的参数类型,泛型可以用在类、接口、方法上。

使用泛型,增加了代码的类型安全性,编译期间做类型检查符合越早出错代价越小的原则;

并且消除了强制类型转换,可以提高性能和代码复用

泛型擦除(泛型基本原理)

泛型的基本原理就是泛型擦除,在编译时会先进行泛型检查,以确保泛型的类型安全,包括类型匹配、类型参数限制等。泛型检查通过后,会进行泛型擦除,将泛型信息从代码中移除,将类型参数替换为上限类型(通常是Object)

以List存储字符串和整形举例,声明一个存储String类型的list,就是将String类型赋给了泛型E,编译器会做泛型检查判断类型是否安全。然后做类型擦除,无论存储String还是Integer,字节码文件中都是Object,也就是向集合中添加的其实都是Object对象,只有当真正使用到集合中的对象时,才会通过checkcast指令完成类型转换

public class Erase {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("111");
        System.out.println(list.get(0));
    }
}

源码
public class ArrayList<E> extends AbstractList<E>
        implements
List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
}

泛型的使用

1.泛型类和方法
泛型类
public class TClass <T>{
    public T data;
    TClass(T data) {
        this.data = data;
    }
    public T getData() {
        return this.data;
    }

    泛型方法,泛型名不要重复,防止外层T被内层T覆盖遮掩
    public <E> void print(E[] array) {
        for (E t : array) {
            System.out.println(t);
        }
    }

    public static void main(String[] args) {
        TClass<String> tClass = new TClass<>("111");
        System.out.println(tClass.getData());
        基本数据类型不能用泛型
        Integer[] a = new Integer[]{1,2,3};
        tClass.print(a);
    }
}

2.泛型接口
interface Container<T> {
    void store(T item);
    T print();
}
public class TInterface<T> implements Container<T>{
    public T item;
    @Override
    public void store(T item) {
        this.item = item;
    }

    @Override
    public T print() {
        return this.item;
    }

    public static void main(String[] args) {
        TInterface<String> tInterface = new TInterface<String>();
        tInterface.store("111");
        System.out.println(tInterface.print());
    }
}

泛型通配符 ?

分为无限制通配符( ? 接受任意泛型类型的数据)、上限通配符( ? extends T 接受任何是T类型或者是其子类型的数据)、下限通配符( ? super T 接受任何是T类型或者是其父类型的数据)

// 无限制通配符示例
public void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

// 上限通配符示例 - T接受的参数是<T extends Number>
public <T extends Number> double sumOfList(List<T> list) {
    double sum = 0;
    for (T item : list) {
        sum += item.doubleValue();
    }
    return sum;
}

// 下限通配符示例
public void addNumbers(List<? super Integer> list) {
    list.add(10);
    list.add(20);
}

Java反射机制

Java反射机制就是在程序运行过程中动态的加载类并获取类的属性和方法,原理基于JVM和类加载机制,通过反射包中的类和接口来实现。

类加载会将类的Class对象存储到内存中,这个对象包含了类的属性和方法等。反射机制需要先拿到这个Class对象,然后才能获取类的构造函数、属性和方法等,之后才能创建实例、访问和修改。

通过反射可以在运行时获取和操作类的信息,灵活性更强,另外还可以进行解耦,比如说Spring中的XML配置就是将依赖关系从代码转移到配置文件中去;存在的问题是性能比直接访问属性和方法底,因为需要在运行时解析类的结构,以及存在安全性问题,因为它可以修改访问权限,甚至可以破坏final类型的不变性。

反射举例

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

class MyClass{
    private String name;
    public final Integer age = 100;
    public MyClass(){};
    public String getInformation(String name) {
        this.name = name;
        return name + " / " + age;
    }
    public String getAll() {
        return name + " / " + age;
    }
}
public class ReflectExample {
    public static void main(String[] args) throws Exception {
        //获取class对象
        //String className = MyClass.class.getName();
        //MyClass myClass = new MyClass();
        Class<?> clazz = Class.forName("反射.MyClass");
        //获取类的构造函数
        Constructor<?> constructor = clazz.getConstructor();
        //创建对象
        Object obj = constructor.newInstance();
        //获取类的方法
        Method method = 
        clazz.getMethod("getInformation", String.class);
        //调用方法并传递参数
        String result = (String) method.invoke(obj, "Bye");
        System.out.println(result);

        //破坏private访问权限
        Field privateName = clazz.getDeclaredField("name");
        privateName.setAccessible(true);
        //修改私有字段值
        privateName.set(obj, "Hello");

        //移除final修饰符
        Field finalAge = clazz.getDeclaredField("age");
        Field modifiers = Field.class.getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.
        setInt(finalAge, finalAge.getModifiers() & ~Modifier.FINAL);
        finalAge.set(obj, 30);

        //做展示
        Method getAll = clazz.getMethod("getAll");
        String resultA = (String) getAll.invoke(obj);
        System.out.println(resultA);
    }
}

输出:
Bye / 100
Hello / 30

获取类Class对象/反射的三种方式

通过类对象的 getClass() 方法获取,对象.getClass()

通过类的静态成员表示,每个类都有隐含的静态成员class,类名.class

通过 Class 类的静态方法 forName() 方法获取,Class.forName(“类的全路径”)

Java创建对象的方式

1.new创建新对象2.通过反射机制3.采用clone机制4.通过序列化机制

Java的IO流

Java的IO主要分为输入流和输出流,按照操作单元可以分为字节流和字符流

Java IO流都是从InputStream 字节输入流、OutputStream 字节输出流、Reader 字符输入流、Writer 字符输出流四个抽象类基类中派生出来的

字符流只能处理纯文本类型,字节流可以处理任意类型,字符流想转为字节流可以在字节流的构造函数中传递字符流对象,这是适配器模式的体现,另外IO流还体现了装饰者模型,通过套娃的方式不改变原有类做功能增强

适配器模型
// 创建字符流
FileReader fileReader = new FileReader("example.txt");
// 使用适配器将字符流转换为字节流
InputStream inputStream = new FileInputStream(fileReader.getFD());

装饰者模式
// 创建文件读取流
FileInputStream fileInputStream = new FileInputStream("example.txt");
// 使用装饰者模式增加缓冲功能
BufferedInputStream bufferedInputStream = 
			new BufferedInputStream(fileInputStream);

BIO、NIO和AIO

BIO(Blocking IO) :它以流的方式处理数据,是同步阻塞式IO,阻塞是指IO操作会导致线程阻塞,直到读写操作完成;同步是指直到IO资源准备好操作系统才会作出响应,意味着IO请求是按照调用的顺序依次执行。它在服务端的实现模式是一个连接一个线程,当有新的连接请求时,服务器会创建一个新线程去处理它,它的性能瓶颈在于阻塞,多线程下需要共用一个accept()去顺序接受连接,线程调用read()时也会阻塞,所以它适用于连接数量小架构比较稳定的场景

NIO(New I/O) :它以包的方式处理数据,是同步非阻塞式IO。它的核心是通道、多路复用、Reactor模型和事件驱动。在Reactor模型中,一个新的连接请求主线程会调用accept进行接收,然后创建一个新的通道注册到选择器Selector上去,主线程不断轮询选择器来检查是否有通道的事件准备就绪,一旦就绪主线程会对事件做处理,一般是开一个新线程去负责事件的处理逻辑,避免影响其他连接请求的处理,它适用于连接数量多但是短连接的场景

AIO( Asynchronous I/O ) :是NIO的一个扩展,异步处理IO操作,采用的是订阅-通知模式,程序向操作系统注册IO监听,然后继续执行任务。当操作系统准备好数据后,通过回调函数CompletionHander()通知程序,支持阻塞和非阻塞两种模式,阻塞是为了和同步IO代码兼容,适用于高并发长连接的场景

BIO服务端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class BIOServer {
    public static void main(String[] args) {
        Integer port = 8080;

        try (ServerSocket serverSocket = new ServerSocket(port)){
            System.out.println("Server is listening on port " + port);
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println
                ("Accepted connection from " + clientSocket);

                BufferedReader reader = new BufferedReader
            (new InputStreamReader(clientSocket.getInputStream()));
                PrintWriter writer = new PrintWriter
            (clientSocket.getOutputStream(), true);

                String clientMessage;
                while ((clientMessage = reader.readLine()) != null) {
                    System.out.println
                    ("Received from client: " + clientMessage);
                    Thread.sleep(5000);
                    writer.println("Server " + clientMessage);
                }
                clientSocket.close();
                System.out.println("Connetion closed");
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
BIO客户端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;

public class BIOClient {
    public static void main(String[] args) {
        String serverAddress = "localhost";
        int serverPort = 8080;

        try (Socket socket = new Socket(serverAddress, serverPort)){
            BufferedReader reader = new BufferedReader
            (new InputStreamReader(socket.getInputStream()));
            PrintStream writer = new PrintStream
            (socket.getOutputStream(), true);

            writer.println("Hello Server1 !");

            String serverResponse1 = reader.readLine();
            System.out.println("Server response: " + serverResponse1);

            writer.println("Hello Server2 !");

            String serverResponse2 = reader.readLine();
            System.out.println("Server response: " + serverResponse2);
        }  catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

客户端和服务端建立了一个连接,然后向服务端快速发送了两个请求,
服务端需要顺序处理,客户端在得到响应之前会进入阻塞

Java序列化

序列化是将对象变成二进制字节流,反序列化就是将二进制字节流恢复成对象,通过它可以方便的进行网络传输和磁盘存储,因为传输过程中无论之前是什么格式都是二进制字节流的形式

实现方式

可以通过实现Serializable接口、Externalizable接口或者使用第三方库如谷歌的Gson。

Serializable接口自动化更强,默认是将所以非transient实例变量都进行序列化;Externalizable接口控制力更强,需要自定义序列化和反序列化字段

可序列化对象

序列化时,只有类的成员变量才能被序列化,静态变量、方法、匿名内部类和transient实例变量都不能被序列化,另外为了防止类已经被序列化,然后对类做修改导致反序列化失败,需要显示的指定序列化版本号(serialVersionUID)

实现Serailizable接口实现序列化和反序列化

//待序列化类
import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    //age序列化前没有,反序列化前添加
    private Integer age;
    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }
}

//序列化和反序列化
import java.io.*;

public class SerializationExample {
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        //序列化
        /*ObjectOutputStream oos = 
        new ObjectOutputStream(new FileOutputStream("person.ser"));
        Person person = new Person("Alice");
        oos.writeObject(person);
        System.out.println("写入完成");
        oos.close();*/

        //反序列化
        ObjectInputStream ois = 
        new ObjectInputStream(new FileInputStream("person.ser"));
        Person newObj = (Person) ois.readObject();
        ois.close();

        System.out.println(newObj.getName() + "/" + newObj.getAge());
    }
}

输出: Alice/null -- 说明类结构改变反序列化也成功了
实现Externalizable接口
public class Person implements Externalizable {
    private String name;
    private String address;
    private Integer age;

    //默认构造函数,反序列化需要
    public Person(){};

    public Person(String name, String address, Integer age) {
        this.name = name;
        this.address = address;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //手动序列化字段
        out.writeObject(name);
        out.writeObject(address);
        out.writeObject(age);
    }

    @Override
    public void readExternal(ObjectInput in) 
    throws IOException, ClassNotFoundException {
        //手动反序列化,根据字段还原数据
        name = (String) in.readObject();
        address = (String) in.readObject();
        age = (Integer) in.readObject();
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

    public String getAddress() {
        return address;
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("person.ser"));
            Person person = new Person("Alice", "北京", 21);
            //调用自定义序列化函数
            person.writeExternal(oos);
            oos.close();
            System.out.println("序列化成功");
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("person.ser"));
            Person newObj = new Person();
            newObj.readExternal(ois);
            ois.close();

            System.out.println("Name: " + newObj.getName());
            System.out.println("Address: " + newObj.getAddress());
            System.out.println("Age: " + newObj.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出:
序列化成功
Name: Alice
Address: 北京
Age: 21

&/&&(| 与 ||同理 )

&运算符有两种用法,分别是按位与和逻辑与。

&&运算符是短路与运算,如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。