《Java核心技术·卷Ⅰ》笔记

776 阅读11分钟

《Java核心技术·卷I》 一些知识点的摘录。

数据类型

数字型

  • Java没有任何无符号(Unsigned)类型的int、long、short、byte等类型;

  • Math类中有更安全的int、long运算,包括addExact(), subtractExact(), multiplyExact(), incrementExact(), decrementExact(), negateExact(),如果溢出会抛出ArithmeticException,原理可以参考zhuanlan.zhihu.com/p/80016248

  • Math.round()方法可以对浮点数进行舍入,返回值是long类型;

  • Java对布尔值的运算中,关系运算符&&与||按短路求值计算,而位运算符&与|不采用短路求值;

  • 赋值运算符=、二元赋值、三元运算符为右结合,从右向左计算;

  • java.math.BigIntegerBigDecimal可以实现任意精度的整数与浮点数运算,但不支持算术运算符,常用的方法有:

    static BigInteger valueof(long x); // 转换为BigInteger或BigDecimal
    int compareTo(BigInteger other); // <返回负数、=返回0、>返回正数
    BigInteger add(BigInteger other);
    BigInteger subtract(BigInteger other);
    BigInteger multiply(BigInteger other);
    BigInteger divide(BigInteger other);
    BigInteger mod(BigInteger other); 
    BigInteger sqrt();
    

字符型

  • java.lang.String 对象是不可变的(immutable),只能让引用这个字符串的变量引用其他的字符串;

  • 要用equals()检测字符串的相等性,==运算符会检测两个字符串是否在同一个位置上,相当于Python的is

  • 空串是长度为0的字符串,而str == null表示没有对象与str关联,要分开检查;

    if(str != null && str.length() != 0)
    
  • 常用的字符串String操作:

    int compareTo(String strCmp); //按字典序比较,<返回负数、=返回0、>返回正数
    boolean empty();
    boolean equals(String strCmp);
    boolean equalsIgnoreCase(String str_cmp);
    String replace(String strOld, String strNew); //查找所有old并用new代替
    String substring(int begIndex, int endIndex);
    String repeat(int repCount);
    static String join(String separator, String[] strs);
    
  • 关于Java中UTF-16与码点的一些特别之处:

    • 注释中的Windows路径C:\users会误以为是\u转义(Unicode转义)而产生错误,因为Unicode转义会在解析代码前进行替换;
    • Java中char默认使用UTF-16编码,建议不要在程序中使用char类型;
    • length()charAt()计算的是代码单元,codePointCount()codePointAt()计算的是码点,比如emoji是1个码点,占2个代码单元,用charAt()就不能正确显示;
  • StringBuilder类用于动态构建字符串:

    int length();
    this append(String str);
    this insert(int offset, String str);
    this delete(int begIndex, int endIndex);
    String toString();
    

 

输入输出

输入

  • 构造一个java.util.Scanner对象,并与System.in关联,即可从控制台读取输入;

    Scanner in = new Scanner(System.in);
    
  • 构造一个java.util.Scanner对象,并与文件名关联,即可从文件中读取输入;

    Scanner in = new Scanner(Path.of("myFile.txt"), StandardCharsets.UTF_8);
    
  • 如果不用Path.of()则Scanner会将字符串解析为数据而不是文件名;

  • Scanner类的常用方法:

    String nextLine(); // 回车分隔
    String next(); // 空格分隔
    int nextInt();
    double nextDouble();
    boolean hasNext();
    boolean hasNextInt();
    boolean hasNextDouble();
    

输出

  • Java 5 沿用了C语言的printf方法,即System.out.printf()

  • 用于printf格式化输出的转换符:

    • d:十进制整数
    • x:十六进制整数
    • f:定点浮点数
    • e:指数浮点数
    • s:字符串
  • 用于printf格式化输出的标志:

    • +:打印正负号
    • 空格:在数字前补空格
    • 0:在数字前补0
    • -:左对齐
    • #:强制f格式包含小数点,或x格式添加前缀0x
    • $:指定参数索引
  • 构造一个java.io.PrintWriter对象,并与文件名关联,即可输出到文件中;

    PrintWriter out = new PrintWriter("myFile.txt", StandardCharsets.UTF_8);
    

 

控制流程

  • Java不能在嵌套的两个块中声明同名的变量;

  • switch语句case的标签可以是:

    • char、byte、short、int的常量表达式
    • 枚举常量
    • 字符串字面量(Java7及以上)
  • Java支持带标签的break语句,标签必须放在希望跳出的最外层循环之前,并且紧跟一个冒号:

    label:
    {
        if(condition)
            break label;
    }
    

 

数组

  • 数组变量的声明与初始化

    int[] a = {1, 2, 3}; // 声明的同时分配空间,不可分开
    int[] a = new int[100]; // 声明与分配空间是分开的,用默认值(数字-0、布尔-false、对象-null)初始化
    int[] a = new int[]{1, 2, 3}; // 声明与分配空间是分开的,用指定值初始化
    
  • 数组拷贝

    int[] newArray = oldArray; // 浅拷贝
    int[] newArray = Arrays.copyOf(oldArray, oldArray.length); // 深拷贝
    
  • java.util.Arrays的常用方法:

    static String toString(T[] a); // 将数组转为字符串
    static String deepToString(T[][] a); // 将二维数组转为字符串
    static T[] copyOf(T[] a, int end);
    static T[] copyOfRange(T[] a, int beg, int end);
    static void sort(T[] a);
    static int binarySearch(T[] a, T v); // 找到返回下标,否则返回-(插入位置+1)
    static int binarySearch(T[] a, int beg, int end, T v);
    static void fill(T[] a, T v);
    static boolean equals(T[] a, T[] b); // 下标相同的元素都对应相等返回true
    

 

对象与类

  • Java中的对象变量可以看作C++中的对象指针,所有Java对象变量都储存在堆中,构造器总是结合new操作符一起使用;

  • Java总是按值调用,但是由于对象变量相当于指针,所以方法得到的变量副本与原变量指向同一个对象;

  • 不要编写返回可变对象引用的访问器方法,会破坏封装性,应该使用clone返回可变对象的副本;

  • Java中的final效果相当于C++中的顶层const;

  • 构造器中使用this(params)调用另一个构造器;

  • Java的包机制类似于C++中的命名空间,package相当于namespace,import相当于using;

  • 如果类、变量、方法不指定public或private,则可以被包内所有方法访问;

  • java.time.LocalDate 常用方法

    static LocalData now(); // 构造一个表示当前日期的对象
    static LocalData of(int year, int month, int day); // 构造一个指定年月日的对象  
    int getYear()/getMonthValue()/getDayOfMonth(); // 获取年/月/日
    int getDayOfWeek().getValue(); // 获取星期几
    LocalDate plusDays(int n)/minusDays(int n); // 生成日期之前/之后n天的日期
    
  • java.util.Objects 用于处理null引用的方法(Java 9)

    // 如果对象为null则抛出带有msg消息的NullPointerException异常
    static <T> void requireNonNull(T obj, String msg); 
    // 如果对象为null则返回默认对象,否则返回对象本身
    static <T> T requireNonNullElse(T obj, T defaultObj); 
    

 

继承

继承

  • 在Java中,所有的继承都是公共继承,没有私有继承与保护继承;

  • super不是一个对象的引用,它只是指示编辑器调用超类方法的特殊关键字;

  • 使用super调用构造器的语句必须是子类构造器的第一句(必须先构造超类);

  • 在Java中,动态绑定是默认的行为,如果不希望一个方法是virtual的,可以声明为final

  • 虚拟机预先为每个类计算了一个方法表,列出了所有方法的签名和要实际调用的方法;

  • abstract关键字用于声明抽象类或抽象方法;

  • 抽象类不能实例化,但可以定义抽象类的对象变量来引用自类的实例;

Object类

  • 可以使用Object类引用所有类型的对象;

  • 基本类型的数组都扩展了Object类;

  • x.toString()可以用""+x代替,这样x也可以是基本类型或null

  • 变参method(Object… params)相当于method(Object[] params),调用相当于method(new Object[]{arg1, arg2, …});

  • java.lang.Object提供的通用方法:

    boolean equals(Object otherObject);
    String toString();
    
  • java.util.Objects的常用方法

    static boolean equals(Object a, Object b); // 允许null的相等性判断
    

泛型数组列表

  • java.util.ArrayList<E>的常用方法

    boolean add(E obj); // 增
    void add(int index, E obj) //插
    E set(int index, E obj); // 改
    E get(int index); // 查
    E remove(int index); // 删
    int size();
    void ensureCapacity(int capacity); // 指定容量
    void trimToSize(); // 削减空位置
    
  • java.util.Arrays的常用方法

    static boolean equals(xxx[] a, xxx[] b); //数组长度相同且对应元素相同才会true
    

装箱与拆箱

  • 包装器都是java.lang.Number类的子类

  • 自动装箱:当基本类型赋值给一个包装器对象的时候,会自动调用包装器的静态方法valueOf(基本类型)

  • 自动拆箱:当一个包装器对象赋值给一个基本类型的时候,会自动调用包装器对象.基本类型Value()

  • 注意==用于包装器对象的时候判断的是它们的地址而非值,应该用equals()方法

  • java.lang.包装器的常用方法,以Integer为例

    int intValue(); // 自动或手动拆箱所用的方法
    static String toString(int i);
    static int parseInt(String s); // 返回字符串s表示的整数
    static Integer valueOf(String s); // 返回用字符串s表示的整数初始化的Integer对象
    

枚举类

  • 枚举类是java.lang.Enum类的子类,常用的公共方法有

    static Enum valueOf(Class enumClass, String name); // 返回给定类中名为name的枚举常量
    String toString();
    int ordinal(); // 返回枚举常量的声明位置,从0开始计数
    int compareTo(Enum otherEnum);
    
  • public enum Gender{ MALE,FEMALE } 的意思是声明一个枚举类Gender,它有且只有MALE和FEMALE两个实例

  • 所以枚举类中可以与普通类一样声明属性、构造函数与方法;

 

接口、Lambda表达式与内部类

接口

  • 接口不是类,而是对希望符合这个接口的类的一组需求;

  • 抽象类偏向于抽象类别或者说事物本质,只能扩展一个抽象类;

  • 而接口偏向于抽象功能或者说事物操作,可以扩展多个接口;

  • 接口可以提供多继承的大多数好处,同时避免了多继承的复杂与低效;

  • 接口中的方法总是public,也必须是public,所以不必再提供访问修饰符了;

  • 接口中的字段总是public static final,同样无需提供多余的关键字了;

  • 可以为接口方法提供一个默认实现,如果实现这个接口的类无需实现这个方法的时候,或者需要兼容老版本接口的时候;

  • instanceof可以检查一个对象是否实现了某个接口;

  • 在标准库中,有成对出现的接口与实用工具类,比如 Collection/CollectionsPath/Paths

  • 在Java8中允许在接口中提供静态方法,这样一来实现接口的时候伴随的实用工具类就不是必须的了;

  • 关于比较接口Comparable

    public interface Comparable<T>{
            int compareTo(T other);
    }
    // java.util.Arrays的sort(Object[] a)方法要求数组元素必须为实现了Comparable接口的类;
    // 如果compareTo将A-B作为整数排序依据的话,运算可能会溢出,可以调用静态方法Interger.compare(A, B);
    

Lambda表达式

  • lambda表达式是一个可传递的代码块;

  • lambda表达式的书写技巧:

    • 即使lambda表达式没有参数,仍然要提供空括号:
    () -> {for(int i; i < 10; ++i) System.out.println(i);}
    
    • 如果可以推导出lambda表达式的参数类型,那么可以忽略其类型:
    Comparator<String> comp = (first, second) -> first.length() - second.length();
    
    • 如果lambda表达式只有一个参数,并且类型可以推导出,那么可以忽略小括号:
    ActionListener listener = event -> System.out.println(Instant.ofEpochMilli(event.getWhen));
    
  • 在Java中对lambda表达式所做的只有将其转换为函数式接口(只有一个抽象方法的接口,比如Comparator<T>);

  • lambda表达式中只能引用值不会改变的变量(事实最终变量,初始化后不会再赋值);

  • java.util.Function中定义了很多非常通用的函数式接口:

    • Predicate接口,ArrayListremoveIf()的参数就是Predicate;    
    public interface Predicate<T>{
            boolean test(T t);
    }
    
    • Supplier接口,只有调用时才会返回一个T类型的值,用于实现懒计算;
    public interface Supplier<T>{
            T get();
    }
    

方法引用

  • 方法引用指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法;

  • 只有当lambda表达式的代码块只有方法调用时才能重写为方法引用;

  • 方法引用有三种传递方式:

    • 对象::实例方法 object::instanceMethod

      例如 System.out::println 等价于 x -> System.out.println(x)

    • 类::实例方法 Class::instanceMethod

      例如 String::compareToIgnoreCase 等价于 (x,y) -> x.compareToIgnoreCase(y)

    • 类::静态方法 Class::staticMethod

      例如 Math::pow 等价于 (x,y) -> Math.pow(x,y)

内部类

  • 内部类可以对同一个包中的其他类隐藏;

  • 内部类方法可以访问外部类作用域中的数据,包括原本私有的数据;

  • 内部类的对象会有一个隐式引用OuterClass.this,指向实例化这个对象的外部类对象;

  • 局部内部类是不带访问说明符的内部类,作用域被限定在声明局部内部类的块中;

  • 局部内部类对外界完全隐藏,但是它可以访问外部类的字段与局部变量(事实最终变量);

  • 匿名内部类是不指定类名的局部内部类,只需创建对象时可以使用:

    // 继承自SuperType类的匿名内部类
    var anonymousInnerClass = new SuperType(construction parameters){
            method and data
    }
    // 实现了InterfaceType接口的匿名内部类
    var anonymousInnerClass = new InterfaceType(){
            method and data
    }
    
  • 利用匿名内部类支持初始化块的特性,可以使用“双括号初始化”:

    // 外层括号建立一个ArrayList的匿名子类,内括号完成了对象的初始化
    invite(new ArrayList<String>(){{add("Henry"); add("Tony");}});
    // 不过更常见的是利用List.of()方法
    invite(List.of("Henry", "Tony"));
    
  • 生成日志或调试信息时往往会用到当前类的名称,利用匿名子类可以获得静态方法的类名:

    // new Object(){}建立一个匿名内部类对象,而getEnclosingClass()方法可以得到其外部类的信息
    new Object(){}.getClass().getEnclosingClass();