后端学习 - 基础 &《Java编程的逻辑》读书笔记

一 基础概念

1 有关Java

  • Java是编译与解释并存的语言:由 Java 编写的程序需要先经过编译步骤,生成字节码文件(.class 文件,面向JVM而非特定系统的),这种字节码必须由 Java 解释器来解释执行
    • 编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低
    • 解释型语言会通过解释器逐句的将代码解释为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢

在这里插入图片描述

2 JVM / JDK / JRE

包含范围从大到小:

  • JDK(Java development kit):功能齐全的 Java SDK。拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序
  • JRE(Java runtime environment):Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是不能用于创建新程序
  • JVM(Java virtual machine):运行 Java 字节码(.class格式文件)的虚拟机,针对不同系统有不同的实现。JVM是一种规范,满足规范的虚拟机都可称为JVM

3 与C++的联系和区别

  • 都是面向对象的语言,支持封装、继承、多态
  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的(接口支持多重继承),C++ 支持多重继承
  • Java 有自动内存管理垃圾回收机制
  • C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载

4 各类型数据占用空间大小

  • Java中比较特殊的是char类型占用2字节(16bit)
  • char 本质上是占用两个字节的无符号整数,对应 Unicode 编号,用于表示对应字符
    在这里插入图片描述

5 == 和 equals() 的区别、hashCode() 方法

  • 对于基本数据类型,只能用 == 比较
  • 对于引用数据类型, == 用于比较内存地址; equals() 如果未被重写,也是比较内存地址,重写后按照指定规则判断两个对象是否相等
  • 重写 equals() 方法时,必须同时重写 hashCode() 方法
  • 两个对象的 hashCode 值相等并不代表两个对象就相等(哈希碰撞)。两个对象相等则 hashCode 必相等
  • 两个对象的比较,首先比较 hashCode() 的返回值是否相等,如果不相等直接认为两个对象不相等,如果相等则继续调用 equals() 方法,返回 True 时视为两个对象相等
  • 如果重写 equals() 时没有重写 hashCode() 方法的话,可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等
  • hashCode() 存在的意义是,减少 equals() 的调用,提高执行速度

6 包装类型

  • 包装类型的比较必须用 equals()
  • 基本数据类型存放在 Java 虚拟机栈中的局部变量表中;而包装类型属于对象类型,存在于堆中
  • 包装类常量池技术
    • Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据
    • Character 创建了数值在 [0,127] 范围的缓存数据
    • Float,Double 没有实现常量池
  • 装箱与拆箱:装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法
Integer i = 10;  //装箱,等价于 Integer i = Integer.valueOf(10)
int n = i;   //拆箱,int n = i.intValue()

在这里插入图片描述

7 final 关键字

  • 被 final 关键字修饰的类不能被继承
  • 修饰的方法不能被重写
  • 修饰的变量是基本数据类型则值不能改变
  • 修饰的变量是引用类型则不能再指向其他对象

8 参数传递机制:值传递

  • Java只存在值传递,如果向方法传递引用类型,则在方法中产生引用类型的堆中的地址的拷贝
public class Person {
    private String name;
   // 省略构造函数、Getter&Setter方法
}

public static void main(String[] args) {
    Person xiaoZhang = new Person("小张");
    Person xiaoLi = new Person("小李");
    swap(xiaoZhang, xiaoLi);
    System.out.println("xiaoZhang:" + xiaoZhang.getName());
    System.out.println("xiaoLi:" + xiaoLi.getName());
}

public static void swap(Person person1, Person person2) {
    Person temp = person1;
    person1 = person2;
    person2 = temp;
    System.out.println("person1:" + person1.getName());
    System.out.println("person2:" + person2.getName());
}

输出:
person1:小李
person2:小张  // 在swap方法中,调换了person1和person2的指向
xiaoZhang:小张
xiaoLi:小李  // 在主方法中,引用指向并未改变
  • swap 方法的参数 person1 和 person2 只是拷贝的实参 xiaoZhang 和 xiaoLi 的地址
  • 因此, person1 和 person2 的互换只是拷贝的两个地址的互换,不会影响到实参 xiaoZhang 和 xiaoLi
  • 另附验证代码:
public class Main {
    public static void change(Person p) {
        //p = new Person("更改后");  // main打印结果是更改前
        p.name = "更改后";  // main打印结果是更改后,直接更改了输入内存地址里的内容
    }
    public static void main(String[] args) {
        Person p_in = new Person("更改前");
        change(p_in);
        System.out.println(p_in.name);
    }
}

class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }
}

9 String 的内存情况

参考 JVM 博客

  • String 类底层使用 char[] 存储
  • String 不同实例化方式的内存对比
    在这里插入图片描述
  • 字符串拼接的内存情况
    str_instance.intern() 将字符串 str 放到常量池中
    在这里插入图片描述

10 访问修饰符

修饰符范围
public无限制
protected子类、当前包
default当前包
private仅限当前类
  • protected 由于其子类的可见性,多用于模板模式

11 引用拷贝、浅拷贝与深拷贝

  • 引用拷贝:创建新的对象引用,指向原来的对象
  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象
  • 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象
    在这里插入图片描述

三 面向对象

1 面向过程与面向对象

  • 面向过程 :面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发
  • 面向对象 :面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护

2 构造方法

  • 类默认具有不带参数的构造方法,如果添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了
    • 无论是否用到,要把无参的构造方法写出来
    • 原因:如果子类构造方法没有显式地调用 super 构造器,则默认调用 super()
  • 子类调用父类的构造方法,使用 super(...),且必须在子类构造方法的首行调用
  • 构造方法不能重写,但是可以重载
  • 通过子类构造器创建对象时,一定会直接或间接地调用父类的构造器,直到调用了 Object 类的构造器,且父类的构造方法先于子类执行

3 OOP特性:封装、继承、多态

  • 封装:封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性
  • 继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  • 多态:编译时类型和运行时类型不一致,具体表现为父类的引用指向子类的实例。引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;多态不能调用“只在子类存在但在父类不存在”的方法,尽管内存中加载了子类特有的属性和方法,想要调用需要向下转型。多态情况下,父类的方法称为虚(拟)方法,调用方法的过程称为动态绑定

4 重载和重写

  • 重载:相同的方法名,不同的参数列表。可以发生在一个类中,也可以发生在父类和子类间。重载就是多个同名方法根据不同的传参来执行不同的逻辑处理重载发生在编译时
  • 重写:相同的方法名,相同的参数列表。发生在父类和子类间。本质上是子类覆盖了父类的方法重写发生在运行时,具体要求:
    1. 抛出异常的类型更小或相等
    2. 访问权限更大或相等
    3. 返回值的类型更小或相等,如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时可以返回该引用类型的子类

5 继承:重名问题

  • 如果重名的属性或方法是 private 修饰的,则父类和子类互不影响
class Base {
    public static String s = "base_static_field";
    public String m = "base_nonstatic_field";
    
    public static void staticTest() {
        System.out.println("base: " + s);
    }
}

class Child extends Base {
    public static String s = "child_static_field";
    public String m = "child_nonstatic_field";
    
    public static void staticTest() {
        System.out.println("child: " + s);
    }
}

public class Verify {
    public static void main(String[] args) {
        Child child = new Child();  // 该对象具有两个声明为 public 的属性 m
        Base base = child;
        // =====子类通过类型转换得到的父类========
        System.out.println(Base.s);
        Base.staticTest();
        System.out.println(base.m);
        // ==========子类=============
        System.out.println(Child.s);
        Child.staticTest();
        System.out.println(child.m);
    }
}

输出结果:
base_static_field
base: base_static_field
base_nonstatic_field

child_static_field
child: child_static_field
child_nonstatic_field

6 继承:实现原理

  • 类加载时需要执行的代码(执行顺序和代码顺序有关)
    • 静态代码块
    • 静态变量声明时的赋值
  • 创建对象时需要执行的代码(粗体执行顺序和代码顺序有关,构造方法最后执行
    • 非静态代码块
    • 非静态变量声明时的赋值
    • 构造方法
  • 寻找要执行的实例方法时,从对象的实际类型开始查找,找不到的时候从父类递归查找
    • 例如调用 base.func() 时,过程如下
      1. base 的实际类型为 Child,在 Child 中找不到 func(),从父类 Base 查找
      2. Base 包含 func(),开始执行
      3. func() 调用 test(),在 Child 中找到 test() ,执行并返回到 func()
      4. func() 返回,执行完成
  • 构造场景如下
public class Verify {
    public static void main(String[] args) {
    	// 创建子类对象
        Child child = new Child();
        // 强转为父类,并调用方法
        Base base = child;
        base.func();
    }
}

class Base {
    static {
        System.out.println("base static block");
    }

    {
        System.out.println("base nonstatic block");
    }

    public Base() {
        System.out.println("base constructor");
    }

    public void func() {
        test();
    }

    public void test() {
        System.out.println("base method call");
    }
}

class Child extends Base {
    static {
        System.out.println("child static block");
    }

    {
        System.out.println("child nonstatic block");
    }

    public Child() {
        System.out.println("child constructor");
    }

    @Override
    public void test() {
        System.out.println("child method call");
    }
}

输出结果:
base static block  // 1.父类加载:执行父类的静态属性赋值->静态代码块
child static block  // 2.子类加载:执行子类的静态属性赋值->静态代码块
base nonstatic block  // 3.父类实例化:执行父类的非静态属性赋值->非静态代码块->构造器
base constructor
child nonstatic block  // 4.子类实例化:执行子类的非静态属性赋值->非静态代码块->构造器【此时子类实例化完成】
child constructor  

child method call  // 调用的是子类而非父类的test()

7 继承的问题:破坏封装

  • 使用继承时,必须严格符合 is-a 的关系,否则会造成混乱
    • 例如,“鸟类”作为父类,拥有“可以飞”的方法,“企鹅”作为子类就是一种不好的实现
  • 子类继承父类时,如果不知道父类的实现细节,就无法正确地进行扩展
    • 解决方法
    1. 使用 final 避免继承
      • final 修饰的方法,父类拥有随意修改方法内部实现的自由
      • final 修饰的类,由于无法被继承,其实现是自由的
    2. 使用组合 + 接口,将父类对象作为子类的一个属性,同时保证父类和子类的一致性
interface Add {
	void plusOne();
}

class Base implements Add {
	@Override
	void plusOne() {
		// ...
	}
}

class Child implements Add {
	Base b;
	
	@Override
	void plusOne() {
		b.plusOne();
		// 方法扩展...
	}
}

8 类的扩展:接口

  • 接口声明了一组能力,但它自己没有对这些能力做出实现,仅是一个约定
    • 继承要求子类和父类间存在 is-a 的关系,而接口关注的是具有某种能力 is-able-to
    • 在某些情况下,代码只关注一个类是否具有某个能力,而不关注具体类型(面向接口编程),可以实现高效的代码复用
  • 接口的一个重要功能是降低了耦合
    • 使用接口的程序依赖于接口本身,而非实现接口的具体类型
    • 可以灵活替换接口实现而不影响接口使用
  • 接口可以有变量,必须且默认被 public static final 修饰
  • 接口可以多继承,类也可以实现多个接口
  • 和类一样,接口可以使用 instanceof 判断某个类是否实现了某个接口
  • JDK8和9的接口增强:接口内可以定义静态方法和默认方法
    • 引入默认方法的目的是,方便给现有的类增加新方法
    • 将新增方法设置为 default,现有的接口实现类无需实现默认方法
public interface Demo {
	// 普通方法声明
	void hello();

	// 静态方法
	public static void hola() {
		System.out.println("hola");
	}
	
	// 默认方法
	default void hi() {
		System.out.println("hi");
	}
}

9 类的扩展:抽象类

  • 接口声明能力;抽象类提供默认实现,实现全部或部分方法,方便子类实现接口
  • 一个接口经常有一个对应的抽象类
  • 抽象类可以定义实例变量,而接口不可以
  • 抽象类有构造方法,但是不能被实例化

10 类的扩展:内部类

  • 内部类只是编辑器的概念,对JVM而言,每个内部类都会被编译成一个独立的类,生成独立的字节码文件
  • 四种内部类
  1. 静态内部类

    • 与外部类关系密切,且不依赖于外部类实例
    • 对于外部类,只能访问外部类的静态属性和方法(包含 private
    • 静态内部类的方法可以设置为静态,也可以非静态,但都只能访问外部类的静态属性和方法
  2. 成员内部类

    • 对于外部类,可以访问外部类的静态和非静态的成员和方法(包含 private
    • 成员内部类对象总是和一个外部类对象相连
      • 成员内部类中不能定义静态的属性和方法
      • 需要先创建外部类对象,再创建内部类对象
    • 一种应用场景是,外部类的方法返回值是某个接口,使用 private 的内部类实现该接口,并在方法中返回,此时的接口实现对外完全隐藏
  3. 方法内部类

    • 应用场景和成员内部类相似,可以由成员内部类代替(如果某个类只在某个方法中被使用,用方法内部类的封装性更好)
    • 方法可以是静态的,也可以是非静态的,对方法内部类的区别在于,能否访问外部类的非静态属性
    • 方法内部类可以访问 final 修饰的方法参数和局部变量
  4. 匿名内部类

public class Verify {
    public static void main(String[] args) {
        // ===1.静态内部类===
        // 实例化静态内部类,调用其非静态方法,仅能访问外部类的静态属性
        Outer.StaticInner staticInner = new Outer.StaticInner();
        staticInner.accessOuterStaticField();
        // 如果暴露的方法是静态方法,则无需创建对象即可执行
        Outer.StaticInner.accessOuterStaticFieldByStaticMethod();

        // ===2.成员内部类===
        // 实例化非静态内部类,并调用其方法,访问外部类的所有属性
        Outer.NonStaticInner nonStaticInner = new Outer().new NonStaticInner();
        nonStaticInner.accessAllOuterField();

        // ===3.方法内部类===
        new Outer().getInstance("param");
    }
}

class Outer {
    // 外部静态属性
    private static String outerStaticField = "outerStaticField";
    // 外部非静态属性
    private String outerNonStaticField = "outerNonStaticField";

    // 外部静态方法
    private static void outerStaticMethod() {
        System.out.println("outerStaticMethod");
    }

    // 外部非静态方法
    private void outerNonStaticMethod() {
        System.out.println("outerNonStaticMethod");
    }

    // 1.静态内部类
    static class StaticInner {
        public void accessOuterStaticField() {
            System.out.println("===StaticInner-NonStaticMethod===");
            outerStaticMethod();  // 访问外部静态方法
            System.out.println(outerStaticField);  // 访问外部静态属性
        }

        public static void accessOuterStaticFieldByStaticMethod() {
            System.out.println("===StaticInner-StaticMethod===");
            outerStaticMethod();  // 访问外部静态方法
            System.out.println(outerStaticField);  // 访问外部静态属性
        }
    }

    // 2.非静态内部类
    class NonStaticInner {
        public void accessAllOuterField() {
            System.out.println("===NonStaticInner===");
            outerStaticMethod();  // 访问外部静态方法
            outerNonStaticMethod();  // 访问外部非静态方法
            System.out.println(outerStaticField);  // 访问外部静态属性
            System.out.println(outerNonStaticField);  // 访问外部非静态属性
        }
    }

    // 3.方法内部类
    SomeAbility getInstance(final String param) {
        final String localParam = "localParam";
        class MethodInner implements SomeAbility {
            // 接口实现...
            public MethodInner() {
                System.out.println("===MethodInner===");
                System.out.println(param);
                System.out.println(localParam);
            }
        }
        return new MethodInner();
    }
}

11 代理模式

静态代理

  • 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
  • 静态代理的实现步骤:定义一个接口及其实现类; 创建一个代理类同样实现这个接口,将目标对象注入进代理类(使其成为代理类的成员变量);然后在代理类的对应方法调用目标类中的对应方法。
  • 静态代理中,对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改且麻烦(需要对每个目标类都单独写一个代理类)。

JDK动态代理(反射的应用)


四 异常

1 异常分类

  • 在编译过程中,如果受检查异常没有被 catch/throw 处理的话,就无法通过编译
  • 未受检异常表示编程时的逻辑错误,应该修改逻辑而非进行异常处理;受检异常表示程序本身没有问题,由于一些不可预测的错误导致的异常
    在这里插入图片描述

2 try-catch-finally

  • try
    • 用于捕获异常
    • 其后可接零个或多个 catch 块,如果没有 catch,则必须有 finally
  • catch
    • 用于处理 try 捕获到的异常
  • finally
    • 无论是否捕获或处理异常,finally 都会被执行
    • 当在 trycatch 中返回时,finally 将在方法返回之前被执行,但不会改变返回值
    static int finallyTest() {
        int i = 1;
        try {
            return i;
        } finally {
            i++;
        }
    }
    // 返回结果是1而不是2
    // 因为执行到 try 内的返回语句时,会将返回值保存在临时变量中,然后执行 finally
    // 返回的是临时变量而非修改后的值
    
    • 只要 finally 语句中有返回语句,finally 语句的内容将被执行(而不是 trycatch 的返回语句),这样会掩盖原返回值或异常,避免在 finally 中返回或抛出异常

3 try-with-resources

  • 在 try 后增加括号,在其中创建资源对象(任何实现 java.lang.AutoCloseable 或者 java.io.Closeable 的对象),不必显式关闭
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
             BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }

五 文件与 I/O 流

1 transient 关键字

  • 作用是阻止实例中那些用此关键字修饰的变量被序列化
    • 当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复
    • transient 只能修饰变量,不能修饰类和方法
    • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值
    • 静态变量因为不属于任何对象,所以无论有没有 transient 关键字修饰,均不会被序列化

2 文件类型:文本文件和二进制文件

  • 文本文件的每个二进制字节都是某个可打印字符的一部分,都可以用最基本的文本编辑器查看和编辑
  • 二进制文件中,每个字节不一定表示字符,可能表示颜色、字体等,如果用基本的文本编辑器直接查看一般是乱码

3 文件读写

  • 操作系统和硬盘交互时,一般按块批量传输,以均摊延时开销
  • 一般读写文件需要两次复制
    • 读文件:硬盘 -> 操作系统内核 -> 应用程序内存
  • 操作系统在操作文件时,一般有打开和关闭的概念
    • 打开文件会在操作系统内核建立一个有关该文件的内存结构,这个结构一般通过一个整数索引引用,这个索引被称为文件描述符
    • 操作系统能同时打开的文件数是有限的,所以在不使用文件时要关闭

4 Java I/O 流

  • Java 具有很多面向流的方法,接受的参数和返回的方法都是流接口对象(面向接口编程,类似容器类的协作体系
  • 一些实际上不是 IO 的数据也或目的地的对象也可以转换为流,以使用流的方法,例如字节数组可以包装为 ByteArrayInputStreamByteArrayOutputStream
  • 序列化和反序列化
    • 序列化就是将内存中的 Java 对象持久地保存到一个流中,反序列化就是从流中恢复 Java 对象到内存
    • 作用:对象的持久化;网络远程调用
    • 通过接口 Serialzable 标记对象是可序列化的
    • 常见的序列化方式:JSON、ProtoBuf

4.1二进制文件 - 字节流 - byte

  • InputStream/OutputStream 基类,抽象类
abstract class InputStream ... {
	// 阻塞直到读取下一个字节
	public abstract int read() throws IOException
	// 阻塞直到读取若干字节,将读到的字节放入参数数组b中
	public abstract int read(byte[] b) throws IOException
	public abstract int read(byte[] b, int off, int len) throws IOException
}

abstract class OutputStream ... {
	// 向流中写入一个字节(仅写入b最低的8位)
	public abstract void write(int b) throws IOException
	// 批量写入
	public abstract void write(byte[] b) throws IOException
	public abstract void write(byte[] b, int off, int len) throws IOException
	// 将缓冲数据写入,默认实现只是将数据传递给操作系统,实际写入时机由操作系统决定
	public void flush() throws IOException
}
  • FileInputStream/FileOutputStream 输入源和输出目标是文件
class FileInputStream extends InputStream ... {
	// 构造方法
	public FileInputStream(File file) throws FileNotFoundException
	public FileInputStream(String name) throws FileNotFoundException
}

class FileOutputStream extends OutputStream ... {
	// 构造方法
	public FileOutputStream(File file, boolean append) throws FileNotFoundException
	public FileOutputStream(String name) throws FileNotFoundException
}
  • ByteArrayInputStream/ByteArrayOutputStream 输入源和输出目标是字节数组
class ByteArrayInputStream extends InputStream ... {
	// 构造方法,适配器模式,将字节数组包装为一个输入流
	public ByteArrayInputStream(byte[] buf)
	public ByteArrayInputStream(byte[] buf, int offset, int length)
}

class ByteArrayOutputStream extends OutputStream ... {
	// 构造方法,底层数组大小会根据数据内容动态扩展
	public ByteArrayOutputStream()
	public ByteArrayOutputStream(int size)
	// 将数据转换为其它类型
	public synchronized byte[] toByteArray()
	public synchronized String toString()
	public synchronized String toString(String charsetName)
	// 写入另一个输出流
	public synchronized void writeTo(OutputStream out) throws IOException
}
// 使用 ByteArrayOutputStream 读文件
public static void main(String[] args) {
	InputStream input = new FileInputStream("hello.txt");
	try {
		ByteArrayOutputStream output = new ByteArrayOutputStream();
		byte[] buf = new byte[1024];
		int bytesRead = 0;
		while ((bytesRead = input.read(buf)) != -1) {  // 从文件输入流读取数据
			output.write(buf, 0, bytesRead);  // 将读到的数据写入字节数组输出流
		}
		String data = output.toString("UTF-8");  // 将字节数组输出流转换为字符串
	} finally {
		input.close();
	}
}
  • DataInputStream/DataOutputStream 以非字节为单位进行读写
    • 是装饰器基类 FilterInputStream/FilterOutputStream 的子类,构造时均需要传入 InputStream/OutputStream 对象
  • BufferedInputStream/BufferedOutputStream 添加缓冲区
    • 使用 FileInputStream/FileOutputStream 时应该总是使用缓冲区包装:InputStream input = new BufferedInputStream(new FileInputStream("hello.txt"));

4.2 文本文件 - 字符流 - char

  • 对于文本文件,字节流没有编码的概念,并且不能按行处理
  • 字符流是按 char 读取的:对于绝大部分字符,一个字符对应一个 char;对于增补集的字符,需要两个 char 表示
  • Reader/Writer 基类,抽象类
  • InputStreamWriter/OutputStreamWriter 适配器类,将 InputStream/OutputStream 转换为 Reader/Writer
public static void main(String[] args) {
	// 输出字节流转换为字符流
	Writer writer = new OutputStreamWriter(new FileOutputStream("hello.txt"), "GB2312");
	try {
		writer.write("hello")
	} finally {
		writer.close();
	}
}
  • FileReader/FileWriter 不能指定编码类型,只能使用默认编码(如果需要指定编码,使用InputStreamWriter/OutputStreamWriter
  • CharArrayReader/CharArrayWriter 输入源和输出目标是字符数组
  • StringReader/StringWriter 本质同上,因为字符串底层是字符数组
  • BufferedReader/BufferedWriter 装饰类,为 Reader/Writer 提供缓冲

六 反射

1 动态语言

  • 动态语言指运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化
  • 反射机制使得Java成为了“准动态语言”:加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息

2 什么是反射

  • 在运行状态中,对于任意一个,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,并且能改变它的属性
  • 反射机制允许程序在运行时取得任何一个已知名称接口、类或实例的内部信息,包括包括其修饰符、属性、方法、接口等,并可于运行时改变属性值或调用方法
  • 反射并不是优先选项
    • 反射容易出现运行时错误,因为编译器无法协助类型检查
    • 反射的性能更低
    • 如果能用接口实现同样目的,优先选择接口

在这里插入图片描述

3 反射常用类与方法

  1. java.lang.Class: 代表一个类
    • 获取 Class 实例不能使用 new,而应该通过 类名.class实例.getClass() 获得
  • 对应关系
Class类
Class实例实例运行时类
实例
    public static void main(String[] args) {
        // 同一个类的不同实例,对应的 Class 实例 是同一个
        Person p1 = new Person("1号");
        Person p2 = new Person("2号");
        Class c1 = p1.getClass();
        Class c2 = p2.getClass();
        Class c3 = Person.class;
        System.out.println(c1 == c2);  // true
        System.out.println(c1 == c3);  // true

        // 数组的元素类型与维度(一维数组,二维数组等)一样,对应的 Class 实例 是同一个
        Class c4 = new String[10].getClass();
        Class c5 = new String[8].getClass();
        Class c6 = new int[10].getClass();
        Class c7 = String[].class;
        System.out.println(c4 == c5);  // true
        System.out.println(c4 == c6);  // false
        System.out.println(c4 == c7);  // true
    }
  1. java.lang.reflect.Method: 代表类的方法
  2. java.lang.reflect.Field: 代表类的属性
  3. java.lang.reflect.Constructor: 代表类的构造器

4 DEMO

package com.ys.reflex;
public class Person {
    // 私有属性
    private String name = "Tom";
    // 公有属性
    public int age = 18;
    // 构造方法
    public Person() {
    }
    // 私有方法
    private void say(){
        System.out.println("private say()...");
    }
    // 公有方法
    public void work(){
        System.out.println("public work()...");
    }
}
  • 获取 Class 对象的三种方式
	// 1.通过对象调用 getClass() 方法来获取
  Person p1 = new Person();
  Class c1 = p1.getClass();

	// 2.直接通过 类名.class 的方式得到,该方法最为安全可靠,程序性能更高
  Class c2 = Person.class;

	// 3.通过 Class 对象的 forName() 静态方法来获取,但可能抛出 ClassNotFoundException 异常
  Class c3 = Class.forName("com.ys.reflex.Person");
  • Class 类具有如下的方法:
    • getName():获得类的完整名字
    • getFields():获得类的 public 类型的属性
    • getDeclaredFields():获得类的所有属性,包括 private 声明的和继承父类的属性
    • getMethods():获得类的 public 类型的方法
    • getDeclaredMethods():获得类的所有方法,包括 private 声明的和继承父类的方法
    • getMethod(String name, Class[] parameterTypes):获得类的特定方法,name 参数指定方法的名字,parameterTypes 参数指定方法的参数类型
    • getConstructors():获得类的public类型的构造方法
    • getConstructor(Class[] parameterTypes):获得类的特定构造方法,parameterTypes 参数指定构造方法的参数类型
    • newInstance():通过类的无参构造方法创建这个类的一个对象
      • 运行时类必须提供空参的构造器;
      • 空参的构造器的访问权限足够
    • 写代码时总是要提供一个 public 的空参构造器,原因:
      • 便于通过反射,创建运行时类的对象
      • 便于子类继承此运行时类时,默认调用 super() 时,保证父类有此构造器
    public void test2() throws Exception{
        Class clazz = Person.class;  // 获取Class对象
        // 1.通过反射,创建Person类的对象
        Constructor cons = clazz.getConstructor(String.class,int.class);  // 获取指定构造器
        Object obj = cons.newInstance("Tom", 12);  // 创建对象
        Person p = (Person) obj;
        
        // 2.通过反射,调用对象指定的属性、方法
        // 访问并修改属性
        Field age = clazz.getDeclaredField("age");
        age.set(p, 10);

        // 调用方法
        Method show = clazz.getDeclaredMethod("show");
        show.invoke(p);  

        // 调用私有构造器
        Constructor cons1 = clazz.getDeclaredConstructor(String.class);
        cons1.setAccessible(true);
        Person p1 = (Person) cons1.newInstance("Jerry");

        // 访问并修改私有属性
        Field name = clazz.getDeclaredField("name");
        name.setAccessible(true);
        name.set(p1, "HanMeimei");

        // 调用私有方法
        Method showNation = clazz.getDeclaredMethod("showNation", String.class);
        showNation.setAccessible(true);
        String nation = (String) showNation.invoke(p1, "中国");

		// 调用静态私有方法
        Method showDesc = clazz.getDeclaredMethod("showDesc");
        showDesc.setAccessible(true);
        Object returnVal = showDesc.invoke(null);
    }

5 反射的应用:JDK动态代理

6 反射与泛型

  • Class 对象中包含泛型信息
public class MyGeneticClass<K extends Comparable<K>, V> {
    K key;
    V value;
    List<String> list;

    public V test(List<? extends Number> numbers) {
        return null;
    }

	public static void main(String[] args) throws Exception {
        Class<MyGeneticClass> c = MyGeneticClass.class;
        // 获取类定义的泛型和边界
        for (TypeVariable t : c.getTypeParameters()) {
            System.out.println("genetic defined by class: " + t.getName() +
                    " extends " + Arrays.toString(t.getBounds()));
        }
        // 属性的泛型
        for (Field field : c.getDeclaredFields()) {
            System.out.println("field: " + field.getGenericType());
        }
        // 方法泛型参数
        Method method = c.getDeclaredMethod("test", new Class[]{List.class});
        System.out.println("method parameter: " + Arrays.toString(method.getGenericParameterTypes()));
        System.out.println("method return: " + method.getGenericReturnType());
    }
}

七 泛型

1 概念和语法

  • 泛型的本质是参数化类型,即数据类型被指定为一个参数
    • 在指定类的泛型为某个参数时,和类的实例有关,因此静态方法不能使用类定义的泛型
    • 如果允许使用,则对于每种实例化类型,都有一份对应的静态属性和方法(但从类型擦除的角度,这些实例化的类型属于一个类),这违反了静态的原则
  • 静态属性和方法中不能使用类的泛型,但静态方法可以自定义泛型,成为泛型方法
    • 泛型方法并不是“使用”了泛型的方法,而是“定义”了新泛型的方法
    • 泛型方法可以用 static 修饰
// 类定义(多个)泛型
class GeneticBase<K, V> {
	// 非静态属性可以使用类定义的泛型
    private K key;
    private V value;

    // 普通方法,可以使用类定义的泛型
    public void commonMethod() {
        // 可以使用 K, V
    }

    // 非静态泛型方法,可以使用类定义的泛型
    public <T> void nonstaticGeneticMethod() {
        // 可以使用 K, V, T
    }

    // 静态泛型方法,不能使用类定义的泛型(泛型类是和实例相关的)
    public static <E> void staticGeneticMethod(E e) {
        // 可以使用 E
    }
}
  • 基本类型不能用于实例化泛型参数,如有需要选择包装类
  • 如果实例化泛型类时,没有指定具体的类型,则认为此泛型的类型为 Object
  • 继承泛型类时,只需在 extends 后的父类后指明泛型类型即可
    • public class SubOrder extends Order<String>
  • 如果不指明,则当前类仍然沿用泛型
    • public class ArrayList<E> extends AbstractList<E>
  • 不能直接创建泛型对象和数组,因为在编译时 T 不是一个具体的类,无法通过编译
    • 创建泛型类:通过反射
    • 创建泛型数组:一般而言泛型容器可以满足需求,如果实在需要数组形式,同样需要反射

2 泛型擦除

  • Java 的泛型是伪泛型,在运行期间,所有的泛型信息都会被擦掉(想象运行时的代码将泛型括号划去,容易理解很多问题
  • 将类型参数擦除,替换为 Object,并进行必要的强制类型转换
  • 从泛型擦除的角度理解:类 A 是类 B 的父类,G<A>G<B> 二者不具备子父类关系;而 A<G>B<G> 的父类
// 由于泛型擦除,不能定义如下的重载方法
public void test(MyGeneticClass<Integer> g);
public void test(MyGeneticClass<String> g);

// 另一种现象
MyGenetic<Integer> i = new MyGenetic(1);
MyGenetic<String> s = new MyGenetic("a");
i.getClass() == MyGenetic.class;  // true
s.getClass() == MyGenetic.class;  // true

// 不支持如下写法
i instanceof MyGenetic<Integer>

// 同样不支持如下写法
class Base implements Comparable<Base> {...}
class Child extends Base implements Comparable<Child> {...}  // 错误,接口不能被实现多次
// ===想改变子类的比较方法,只能@Override===
class Child extends Base {
	@Override
	public int compareTo(Base o) {
		if (!o instance of Child) {
			throw new IllegalArgumentException();
		}
		Child c = (Child) o;
		// ...
	}
}

八 Java8 新特性:函数式编程、Lambda 表达式、Stream API

1 基本概念:函数式接口、函数式编程

  • 函数式接口
    • 仅包含**一个抽象方法(可以包含静态、默认方法,但抽象方法只有一个)**的接口
    • 因为 Java 代码无法直接传递,函数式接口本质上相当于一个方法(一段代码),通过接口的方式作为参数、返回值进行传递,即:逻辑上传递代码,实际上传递接口
    • 使用 @FunctionalInterface 标注函数式接口
  • 函数式编程
    • 函数可以作为参数传递、作为返回值、通过组合构建新的函数
@FunctionalInterface
public interface MyFunctionalInterface {
    /**
     * 用于执行二元运算
     * @param a 参数1
     * @param b 参数2
     * @return 运算结果
     */
    int calculate(int a, int b);
}

public class FunctionalProgramming {
    public static void main(String[] args) {
        int result = Calculator.doCalculate(1, 2, (a, b) -> a + b);  // lambda表达式补全函数式接口
        System.out.println(result);
    }

    private static class Calculator {
        private static int doCalculate(int a, int b, MyFunctionalInterface functionalInterface) {
            return functionalInterface.calculate(a, b);
        }
    }
}

2 Lambda 表达式

  • Lambda 表达式可以用于代码传递过程,即 Lambda 表达式可以赋值给函数式接口,如下所示
  • 和匿名内部类相比,Java 会为每个匿名内部类生成一个类,但不会为 Lambda 表达式生成类,所以效率更高
    • 同时可以得出,Lambda 表达式的内部实现不是匿名内部类,所以也不是语法糖
// Comparator 是典型的函数式接口
Comparator<Integer> comparator = (a, b) -> a - b;  // lambda表达式赋值给函数式接口

// 匿名内部类写法 和 Lambda表达式写法
Collections.sort(list, new Comparator<Integer>() {
	@Override
	public int compare(int a, int b) {
		return a - b;
	}
)
Collections.sort(list, (int a, int b) -> {  // Lambda表达式完整语法
	return a - b;
});
Collections.sort(list, (a, b) -> a - b);  // Lambda表达式简化语法
  • Lambda 表达式只能访问 final 类型的局部变量,因为 Lambda 表达式会为参数建立副本
    • 因为局部变量定义在栈中,Lambda 表达式执行的时候可能方法已经执行结束(栈帧弹出),所以必须要为参数建立副本

3 预定义的函数式接口

  • Java 8 预定义了大量函数式接口,这些接口广泛用于 Stream API,也可以自定义使用
函数接口方法作用
Predicate<T>boolean test(T t)测试输入是否满足条件
Function<T, R>R apply(T t)对输入执行类型转换,将输入类型T转为R
Consumer<T>void accept(T t)消费者
Supplier<T>T get()工厂方法
public class FunctionalProgramming {
    public static void main(String[] args) {
        List<Integer> nums = new LinkedList<>();
        nums.add(0);
        nums.add(1);
        nums.add(2);

        // 1.predicate:获取大于1的元素
        List<Integer> filter = filter(nums, a -> a > 1);  // 自定义写法
        List<Integer> collect = nums.stream()  // StreamAPI写法
                .filter((a) -> a > 1)
                .collect(Collectors.toList());

        // 2.function:将Integer转为String
        List<String> transfer = transfer(nums, a -> a.toString());  // 自定义写法
        List<String> stringList = nums.stream()  // StreamAPI写法
                .map(a -> a.toString())
                .collect(Collectors.toList());

        // 3.Consumer:获取任务并执行,此处的任务是打印
        consume(nums, a -> System.out.println(a));  // 自定义写法
        nums.stream().peek(a -> System.out.println(a));  // StreamAPI写法
    }

    private static <E> List<E> filter(List<E> list, Predicate<E> predicate) {
        List<E> result = new LinkedList<>();
        for (E element : list) {
            if (predicate.test(element)) {
                result.add(element);
            }
        }
        return result;
    }

    private static <T, R> List<R> transfer(List<T> list, Function<T, R> function) {
        List<R> result = new ArrayList<>(list.size());
        for (T t : list) {
            result.add(function.apply(t));
        }
        return result;
    }

    private static <T> void consume(List<T> t, Consumer<T> consumer) {
        for (T element : t) {
            consumer.accept(element);
        }
    }
}

4 Stream API 与函数式数据处理

  • Stream 类相当于是一个迭代器,但提供了更为丰富的操作
    • 定义了许多处理数据的基本函数,完善且屏蔽了底层细节(如没有显式的循环),抽象层次更高
    • 解决问题的主要思路是组合利用基本函数,类似 SQL 语句
    • 声明式而非命令式的方式实现功能
public class FunctionalProgramming {
    public static void main(String[] args) {
    	// 对列表执行过滤和去重操作
        List<Integer> nums = new LinkedList<>();
        nums.add(0);
        nums.add(1);
        nums.add(1);
        nums.add(2);

        // 声明式
        List<Integer> result = nums.stream()
                .filter((a) -> a > 1)  // 返回Stream<Integer>
                .distinct()  // 返回Stream<Integer>
                .collect(Collectors.toList());  // 返回List<Integer>

		// 命令式
		for (Integer i : nums) {
			...
		}
    }
}

  • 中间操作与终端操作
    • 执行中间操作 filter, map, distinct... 返回值都是 Stream 类型,不会执行任何实际的操作,而只是在构建操作的流水线
    • 执行终端操作 max, min, count, collect... 才会触发遍历执行,通过一次遍历完成过滤、转换、收集等工作
中间操作方法作用
Stream<T> distinct()去重
Stream<T> filter((Predicate<? super T> predicate)过滤
Stream<R> map(Function<? super T, ? extends R> mapper)改变类型
Stream<T> sorted(Comparator<? super T> comparator)排序
Stream<T> skip(long n)跳过
Stream<T> limit(long maxSize)截取
Stream<T> peek(Consumer<? super T> action)将流传递给消费者进行处理(调试),但返回值不变
终端操作方法作用
Optional<T> max/min(Comparator<? super T> comparator)获取最大值、最小值
long count()计数
boolean allMatch/anyMatch/noneMatch(Predicate<? super T> predicate根据传入条件的匹配情况
  • 数组也可以转换为流,并使用流数据处理 Stream<T> Arrays.stream(T[] array)
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值