用亿点时间重学部分Java基础

1,258 阅读9分钟

本文中的部分内容借鉴JavaGuide

从笔者个人角度, 针对Java基础中的一些生僻难点进行了归类整理, 希望可以帮到有需要的兄弟们

1 关键字

1.1 private

类中所有的private方法都隐式地指定为final

1.2 static

static修饰的变量永远不会被序列化

类中被static修饰的内容建议通过类名进行调用

通过静态导入import static导入的静态成员, 可以直接使用, 不需要再通过类名调用

1.3 strictfp

strictfp关键字可以应用于方法/类/接口, 不能应用于抽象方法/变量/构造函数, 确保在每个平台上获得相同的结果

常用于浮点数相关内容, 防止因平台不同导致数据精度不一致

Java 17中解决了浮点指令问题, 已移除此关键字

1.4 transient

对于不想序列化的变量, 使用transient

需要注意以下内容:

  • 只能修饰变量, 不能修饰类和方法
  • 修饰的变量, 在反序列化后变量值将会被置成类型的默认值. 例如, 如果是修饰 int 类型,那么反序列化后结果就是 0

2 基本类型

2.1 8种基本类型介绍

基本类型位数字节默认值
int3240
short1620
long6480L
byte810
char162'u0000'
float3240f
double6480d
boolean1false

其中boolean依赖于JVM厂商的具体实现, 理论上占1位, 可能出现不同

2.2 精度丢失问题

floatdouble都会丢失精度, 原因是数据转换为二进制可能会出现无限循环, 超出储存长度

在涉及金额等极为敏感的浮点数据时, 请使用BigDecimal来处理运算

在创建BigDecimal时, 应使用字符串参数或valueOf方法:

// YES!!
new BigDecimal("0.1");
BigDecimal.valueOf(0.1f);
// NO!! 会丢失精度
new BigDecimal(0.1f);

2.3 包装类型

基本类型都有对应的包装类型, 其存在的意义在于允许null值含义

举个例子, 学生参加考试但成绩为0没有参加考试成绩为null是两种情形, 基本类型int没办法体现, 其包装类型Integer则可以

包装类型除FloatDouble外都存在常量池, 所以在比较时应使用equals方法而不是==:

// 常量池中存在一个Integer对象, 其值为1
Integer i1 = 1;
Integer i2 = 1;
Integer i3 = new Integer(1);
// 结果为true
i1 == i2;
// 结果为false
i1 == i3;
// 结果为true
i1.equals(i3);

3 运算

3.1 位运算

测试变量:

// 0011 1100
a = 60
// 0000 1101
b = 13

>> 右移

// 111
a >> 3

每移动一位都可以理解成除2取商:

60/2 = 30
30/2 = 15
15/2 = 7

>>> 右移补零

// 0000 0111
a >>> 3

>>的区别为是否补零

|

把二进制中0理解为false, 1理解为true, |理解为||

// 0011 1100
// 0000 1101
// 0011 1101
a | b

4 函数

4.1 函数签名

函数签名是函数在一个类中的唯一标识

内容包括方法名/参数类型/参数名, 不包括返回值

4.2 重载

发生在同一个类中(或者父类和子类之间), 方法名必须相同,参数类型不同/个数不同/顺序不同,方法返回值和访问修饰符可以不同

那可以存在方法名相同/参数相同但返回值不同的重载吗?

答案是不行, 3.1中的函数签名不包括返回值, 所以方法名相同/参数相同但返回值不同的方法会被认为是同一方法, 不能进行重载

4.3 重写

方法名/参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类

如果方法的返回值类型是 void 和基本数据类型,则返回值重写时不可修改

如果方法的返回值类型是引用类型,重写时可以返回该引用类型的子类

4.4 try-catch-finally

try语句和finally语句中都有return时, finally语句的返回值将会覆盖原始的返回值

换而言之, finally语句一定会执行

// 返回为0
try {
    return 1;
} catch (Exception e) {
    // ...
} finally {
    return 0;
}

4.5 try-with-resource

适用于任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象, 会自动调用close方法

5 面向对象

5.1 对象构建顺序

  1. 静态代码块
  2. 非静态代码块
  3. 构造方法

5.2 静态代码块

public class Person {
    
    static {
        // 静态代码块
    }
    
}

静态代码只会在类的初始化步骤执行一次, 具体在代码中写法为:

  • new Person()
  • Class.forName("cn.houtaroy.models.Person")

举个具体的例子:

package cn.houtaroy.models;
​
public class Person {
    
    static {
        System.out.println("静态代码块执行");
    }
    
    public static void main(String[] args) {
        try {
            new Person();
            Class.forName("cn.houtaroy.models.Person");
            new Person();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
}

上述代码只会有一次输出: 静态代码块执行

可以理解为: 静态代码块中定义的是不同对象共性的初始化内容

一个类中的静态代码块可以有多个,会按照它们出现的先后顺序依次执行

public class Person {
    
    static {
        // 先执行
    }
    static {
        // 再执行
    }
    
}

静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问:

public class Person {
    
    static {
        // 可以赋值
        age = 0;
        // IDE报错, 无法访问
        System.out.printf("出生年龄: %d", age);
    }
    
    public static Integer age;
    
}

5.3 静态内部类

  1. 不需要依赖外围类的创建
  2. 不能使用任何外围类的非静态成员变量和方法

静态内部类可用于实现单例模式, 优点是延迟初始化JVM提供的线程安全支持 :

public class PersonFactory {
​
    // 私有构造方法, 防止在外部调用
    private PersonFactory() {
        
    }
​
   // 私有静态内部类, 无法被外部访问
    private static class PersonFactoryHolder {
        private static final PersonFactory INSTANCE = new PersonFactory();
    }
​
    // 静态方法, 调用私有静态内部类获取唯一实例
    public static PersonFactory getInstance() {
        return PersonFactoryHolder.INSTANCE;
    }
    
}

5.4 hashCodeequals

hashCode用于判断对象的hash值是否相同

equals用于判断对象是否相同

在对二者进行重写时, 请依据以下原则:

  1. 两个对象相等, hashCode返回值一定相同
  2. 两个对象相等, 使用equals返回true
  3. 两个对象hashCode返回值相同,它们也不一定相等
  4. equals被覆盖过,则hashCode方法也必须被覆盖
  5. hashCode的默认行为是对堆上的对象产生独特值, 如果没有重写hashCode, 则该类的两个对象无论如何都不会相等

5.5 值传递

Java中只有值传递, 哪怕是在面向对象, 举个具体的例子:

public class Test {
​
    public static void main(String[] args) {
        Student s1 = new Student("小张");
        Student s2 = new Student("小李");
        Test.swap(s1, s2);
        System.out.println("s1:" + s1.getName());
        System.out.println("s2:" + s2.getName());
    }
​
    public static void swap(Student x, Student y) {
        Student temp = x;
        x = y;
        y = temp;
        System.out.println("x:" + x.getName());
        System.out.println("y:" + y.getName());
    }
    
}

输出结果为:

x:小李
y:小张
s1:小张
s2:小李

可以看到, 在函数内部xy进行了交换, 但外部的s1s2并没有发生变化

所以这四个变量代表的含义是:

  • s1: 学生小张对象的引用的值
  • s2: 学生小李对象的引用的值
  • x: s1的深拷贝
  • y: s2的深拷贝

因为xy是深拷贝, 所以无论它们如何变化, 都不会影响原来的s1s2

5.6 this/super与静态

两者的概念范畴完全不同:

  • 静态方法是类范畴的概念
  • this/super是对象范畴的概念

5.7 Objects.equals

在进行比较时(并非类, 全部内容均可)推荐使用Objects.equals:

// 均为false, 且不会抛空指针异常
Objects.equals("Houtaroy", null);
Objects.equals(null, "Houtaroy");

6 枚举

6.1 使用枚举相关集合

在将枚举作为元素或键值等使用时, 推荐使用枚举相关集合, 例如EnumSetEnumMap:

// 创建EnumSet
EnumSet<Gender> set = EnumSet.of(Gender.MAN);
// 创建EnumMap
EnumMap<Gender, String> map = new EnumMap<>(Gender.class);
map.put(Gender.MAN, "男人");

6.2 实现设计模式

单例模式

利用枚举可以更简洁/高效/安全的实现单例模式, 且由JVM提供保障

public enum PersonFactory {
    
    INSTANCE;
​
    PersonFactory() {
      // 实现人员工厂初始化
      person = PersonCheckEntity.builder().id("test").build();
    }
​
    private PersonCheckEntity person;
​
    public static PersonFactory getInstance() {
      return INSTANCE;
    }
​
    public PersonCheckEntity getDeliveryStrategy() {
      return this.person;
    }
    
}

策略模式

public enum PersonStrategy {
    
    STAND {
        @Override
        public void advance(Person person) {
            System.out.println("向前迈了一步");
        }
    },
    SIT {
        @Override
        public void advance(Person person) {
            System.out.println("向前爬了一截");
        }
    };
 
    public abstract void advance(Person person);
    
}
​
public class Person {
    
    private PersonStrategy status;
    
    public void advance() {
        status.advance(this);
    }
    
}

状态模式

public enum PersonStrategy {
    
    STAND {
        @Override
        public void walk(Person person) {
            System.out.println("向前迈了一步");
        }
    },
    SIT {
        @Override
        public void walk(Person person) {
            System.out.println("坐着没办法走路, 站起来");
            person.setStatus(PersonStrategy.STAND);
            person.walk();
        }
    };
 
    public abstract void walk(Person person);
    
}
​
public class Person {
    
    private PersonStrategy status;
    
    public void walk() {
        status.walk(this);
    }
    
}

7 反射

7.1 什么是反射

反射是框架的灵魂

它赋予了程序在运行过程中分析和使用类概念的能力, 使我们脱离最基本的业务逻辑, 站在更高的维度去处理和思考问题

Java中的利器注解便用到了反射

7.2 获取Class对象的四种方式

Class对象可以理解为类的描述

具体类

Class myClass = Target.class;

Class.forName

Class myClass = Class.forName("cn.houtaroy.models.Target");

对象实例

Target object = new Target();
Class myClass = object.getClass();

类加载器

Class myClass = ClassLoader.loadClass("cn.houtaroy.models.Target");

通过类加载器获取的Class不会执行初始化, 意味着不进行包括初始化等一系列步骤,静态块和静态对象不会执行

7.3 具体操作

创建目标类:

public class Target {
    
    private String value;
​
    public void say(String name) {
        System.out.printf("I love %s%n", name);
    }
​
    private void classValue() {
        System.out.printf("value is %s%n", value);
    }
    
}

进行反射操作:

public class ExampleUtils {
    
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException,
        NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        Class<?> targetClass = Class.forName("cn.houtaroy.models.Target");
        Object targetObject = targetClass.newInstance();
        Method[] methods = targetClass.getMethods();
        for (Method method : methods) {
            System.out.printf("拥有方法: %s%n", method.getName());
        }
        Field[] fields = targetClass.getFields();
        for (Field field : fields) {
            System.out.printf("拥有字段: %s%n", field.getName());
        }
        targetClass.getDeclaredMethod("say", String.class).invoke(targetObject, "Java");
        Field valueField = targetClass.getDeclaredField("value");
        valueField.setAccessible(true);
        valueField.set(targetObject, "test");
        Method classValueMethod = targetClass.getDeclaredMethod("classValue");
        classValueMethod.setAccessible(true);
        classValueMethod.invoke(targetObject);
    }
    
}

运行结果为:

拥有方法: say
拥有方法: wait
拥有方法: wait
拥有方法: wait
拥有方法: equals
拥有方法: toString
拥有方法: hashCode
拥有方法: getClass
拥有方法: notify
拥有方法: notifyAll
I love Java
value is test
  • getMethodsgetFields方法只会读取public属性
  • 访问私有属性时使用getDeclaredMethodgetDeclaredField, 并调用setAccessible(true)修改其可使用性

从上述内容即可看出, 利用反射会带来一定程度的安全隐患

8 Java中的IO

程序I/O可分解为如下操作:

  1. 程序向操作系统发起I/O调用请求
  2. 系统内核等待I/O设备准备好数据
  3. 系统内核将数据从内核空间拷贝到用户空间

8.1 Blocking I/O

即同步阻塞I/O, 程序会一直等待到I/O操作执行完成

8.2 Non-blocking I/O

即I/O多路复用模型

同步非阻塞I/O使用轮询方式, 会非常消耗CPU资源, 在Web开发中几乎无法使用

I/O多路复用模型利用一个线程来管理, 通过减少无效的系统调用,减少了对 CPU 资源的消耗

8.3 Asynchronous I/O

即异步I/O模型, 通俗点就是系统内核完成后会执行程序指定的回调函数

9 小细节

9.1 StringBuilderStringBuffer区别

StringBuilder线程不安全

StringBuffer线程安全

9.2 字节流与字符流

字节(Byte)是计量单位,表示数据量多少,是计算机信息技术用于计量存储容量的一种计量单位,通常情况下一字节等于八位

字符(Character)是计算机中使用的字母、数字、字和符号, 在不同的编码中占用的字节数量不同

Java中存在字节流为什么还要提供字符流呢?

因为在不知道字符编码类型时, 使用字节流很容易出现乱码问题

音频文件、图片等媒体文件用字节流较好,如果涉及到字符的话使用字符流较好