Skip to content

Summary

使用了当前最新的技术和工具、推荐的使用/配置方式和最佳实践。

Java是介于编译型语言和解释型语言之间。编译型语言如C、C++,代码是直接编译成机器码执行,但是不同的平台(x86、ARM等)CPU的指令集不同,因此,需要编译出每一种平台的对应机器码。解释型语言如Python、Ruby没有这个问题,可以由解释器直接加载源码然后运行,代价是运行效率太低。而Java是将代码编译成一种“字节码”,它类似于抽象的CPU指令,然后,针对不同平台编写虚拟机,不同平台的虚拟机负责加载字节码并执行,这样就实现了“一次编写,到处运行”的效果。

  • Java SE:Standard Edition

  • Java EE:Enterprise Edition

  • Java ME:Micro Edition

简单来说,Java SE就是标准版,包含标准的JVM和标准库,而Java EE是企业版,它只是在Java SE的基础上加上了大量的API和库,以便方便开发Web应用、数据库、消息服务等,Java EE的应用使用的虚拟机和Java SE完全相同。

Java ME就和Java SE不同,它是一个针对嵌入式设备的“瘦身版”,Java SE的标准库无法在Java ME上使用,Java ME的虚拟机也是“瘦身版”。

  • JDK:Java Development Kit
  • JRE:Java Runtime Environment

JRE就是运行Java字节码的虚拟机。但是,如果只有Java源码,要编译成Java字节码,就需要JDK,因为JDK除了包含JRE,还提供了编译器、调试器等开发工具。

二者关系如下:

继承

如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。

eg:

java
public class Main {
    public static void main(String[] args) {
        Student s = new Student("Xiao Ming", 12, 89);
    }
}

class Person {
    protected String name;
    protected int age;

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

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        this.score = score;
    }
}

运行上面的代码,会得到一个编译错误,大意是在Student的构造方法中,无法调用Person的构造方法。

这是因为在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();,所以,Student类的构造方法实际上是这样:

java
class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(); // 自动调用父类的构造方法
        this.score = score;
    }
}

但是,Person类并没有无参数的构造方法,因此,编译失败。

解决方法是调用Person类存在的某个构造方法。例如:

java
class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(name, age); // 调用父类的构造方法Person(String, int)
        this.score = score;
    }
}

即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。

在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface。 interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有.

java
class Student implements Person, Hello { // 实现了两个interface
}

包没有父子关系。java.utiljava.util.zip是不同的包,两者没有任何继承关系。

类型

基本类型:byte,short,int,long,boolean,float,double,char

引用类型:所有classinterface类型

引用类型可以赋值为null,表示空,但基本类型不能赋值为null

字符串

Java的Stringchar在内存中总是以Unicode编码表示。

比较:必须使用equals()方法而不能用==

java
public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
    }
}

两个字符串用==equals()比较都为true,但实际上那只是Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然s1和s2的引用就是相同的。

所以,这种==比较返回true纯属巧合

引用类型比较,要使用equals()方法,如果使用==比较,它比较的是两个引用类型的变量是否是同一个对象。因此,引用类型比较,要始终使用equals()方法,但enum类型可以例外。

这是因为enum类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==比较:

异常处理

异常是一种class,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了:

java
try {
    String s = processFile(“C:\\test.txt”);
    // ok:
} catch (FileNotFoundException e) {
    // file not found:
} catch (SecurityException e) {
    // no read permission:
} catch (IOException e) {
    // io error:
} catch (Exception e) {
    // other error:
}

Throwable是异常体系的根,它继承自ObjectThrowable有两个体系:Error和Exception,Error表示严重的错误,程序对此一般无能为力,例如:

  • OutOfMemoryError:内存耗尽
  • NoClassDefFoundError:无法加载某个Class
  • StackOverflowError:栈溢出

Exception则是运行时的错误,它可以被捕获并处理。

Exception又分为两大类:

  • RuntimeException以及它的子类;
  • 非RuntimeException(包括IOException、ReflectiveOperationException等等)

Java规定:

  • 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。

  • 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。

存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。例如:

java
public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException e) {
        System.out.println("IO error");
    } catch (UnsupportedEncodingException e) { // 永远捕获不到
        System.out.println("Bad encoding");
    }
}

UnsupportedEncodingException异常是永远捕获不到的,因为它是IOException的子类。当抛出UnsupportedEncodingException异常时,会被catch (IOException e) { ... }捕获并执行。

NullPointerException

空指针异常,俗称NPE,如果一个对象为null,调用其方法或访问其字段就会产生NullPointerException,这个异常通常是由JVM抛出的,例如:

java
public class Main {
    public static void main(String[] args) {
        String s = null;
        System.out.println(s.toLowerCase());
    }
}

Java语言中并无指针。我们定义的变量实际上是引用,Null Pointer更确切地说是Null Reference

NPE是一种代码逻辑错误,遵循原则是早暴露,早修复,严禁使用catch来隐藏这种编码错误:

java
try {
    transferMoney(from, to, amount);
} catch (NullPointerException e) {
}

反射

反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。 反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。

泛型

eg:

java
ArrayList<String> strList = new ArrayList<String>();

泛型接口

除了ArrayList<T>使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>这个泛型接口:

java
public interface Comparable<T> {
    /**
     * 返回负数: 当前实例比参数o小
     * 返回0: 当前实例与参数o相等
     * 返回正数: 当前实例比参数o大
     */
    int compareTo(T o);
}

eg: 对Person排序

java
public class Person implements Comparable<Person> {

	@Range(min = 1, max = 20)
	public String name;

	@Range(max = 10)
	public String city;

	@Range(min = 1, max = 100)
	public int age;

	public Person(String name, String city, int age) {
		this.name = name;
		this.city = city;
		this.age = age;
	}

	@Override
	public String toString() {
		return String.format("{Person: name=%s, city=%s, age=%d}", name, city, age);
	}

	@Override
	public int compareTo(Person other) {
		// 按年龄升序排列
		return this.age - other.age;
	}
}
java
import java.util.Arrays;


public class Main {

	public static void main(String[] args) throws Exception {
		Person p1 = new Person("Bob", "Beijing", 12);
		Person p2 = new Person("Tom", "Shanghai", 24);
		Person p3 = new Person("Alice", "Chengdu", 199);
		
		for (Person p : new Person[] { p1, p2, p3 }) {
			try {
				check(p);
				System.out.println("Person " + p + " checked ok.");
			} catch (IllegalArgumentException e) {
				System.out.println("Person " + p + " checked failed: " + e);
			}
		}
		Person[] peoples = { p3, p2, p1 };
		// 排序
		Arrays.sort(peoples);
		System.out.println(Arrays.toString(peoples));
	}

  // 结合反射对注释的使用
	static void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
		for (Field field: person.getClass().getFields()) {
			Range range = field.getAnnotation(Range.class);

			if (range != null) {
				Object value = field.get(person);
				if (value instanceof String s) {
					if (s.length() < range.min() || s.length() > range.max()) {
						throw new IllegalArgumentException("Invalid field:" + field.getName());
					}
				} else if (value instanceof  Integer i) {
					if (i < range.min() || i > range.max()) {
						throw new IllegalArgumentException("Invalid field:" + field.getName());
					}
				}
			}
		}
	}
}

自定义注释

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

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {

	int min() default 0;

	int max() default 255;

}

静态方法

编写泛型类时,要特别注意,泛型类型<T>不能用于静态方法,因为:

  • 泛型类型是在实例化对象时确定的,而静态方法是在类加载时就可以直接调用的,无需创建对象实例。所以静态方法中的返回值、参数等不能依赖泛型类型<T>,必须将静态方法的泛型类型和实例类型的泛型类型区分开。
  • 此外,静态方法是与类本身相关联的,而不是与类的实例相关联的。因此,即使创建了一个类的实例,静态方法也无法访问实例的泛型类型。
java
public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    // 静态泛型方法应该使用其他类型区分:
    public static <K> Pair<K> create(K first, K last) {
        return new Pair<K>(first, last);
    }
}

擦拭法

Java语言的泛型实现方式是擦拭法(Type Erasure)。擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

  • 编译器把类型<T>视为Object
  • 编译器根据<T>实现安全的强制转型。

使用泛型的时候,我们编写的代码也是编译器看到的代码:

java
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

而虚拟机执行的代码并没有泛型:

java
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

Java泛型的局限

  • <T>不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型:
  • 无法取得带泛型的Class,获取到的是同一个Class,eg: Pair<Object>
  • 无法判断带泛型的类型
  • 不能实例化T类型

<? extends Number>通配符的一个重要限制:方法参数签名setFirst(? extends Number)无法传递任何Number的子类型给setFirst(? extends Number)

java
  public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        int n = add(p);
        System.out.println(n);
    }

    static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        // 会报错
        p.setFirst(new Integer(first.intValue() + 100));
        p.setLast(new Integer(last.intValue() + 100));
        return p.getFirst().intValue() + p.getFirst().intValue();
    }

这里唯一的例外是可以给方法参数传入null

java
p.setFirst(null); // ok, 但是后面会抛出NullPointerException
p.getFirst().intValue(); // NullPointerException

定义泛型类型Pair<T>的时候,也可以使用extends通配符来限定T的类型:

public class Pair<T extends Number> { ... }, then, 只能定义这些:

java
Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Double> p3 = null;

Number的子类

相应的有super, <? super T>, 两者的区别在于:

  • <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);

  • <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

一个是允许读不允许写,另一个是允许写不允许读。

eg: Java标准库的Collections类定义的copy()方法:

java
public class Collections {
    // 把src的每个元素复制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}

PECS原则

何时使用extends,何时使用super?可以用PECS原则:Producer Extends Consumer Super。

上面的 src是producer, dest是consumer

其实PECS原因就是java向上转型安全,而向下转型不安全

无限定通配符

java
void sample(Pair<?> p) {
}
  • 不允许调用set(T)方法并传入引用(null除外);
  • 不允许调用T get()方法并获取T引用(只能获取Object引用)。

既不能读,也不能写,那只能做一些null判断:

java
static boolean isNull(Pair<?> p) {
    return p.getFirst() == null || p.getLast() == null;
}

大多数情况下,可以引入泛型参数<T>消除<?>通配符:

java
static <T> boolean isNull(Pair<T> p) {
    return p.getFirst() == null || p.getLast() == null;
}

IOC & DI

IoC(Inversion of Control)又称为依赖注入(DI:Dependency Injection),它解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。

集合

java
public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("A", "B", "C");
        System.out.println(list.contains(new String("C"))); // true or false?
        System.out.println(list.indexOf(new String("C"))); // 2 or -1?
    }
}

结果是true & 2,是不同的实例,但List内部并不是通过==判断两个元素是否相等,而是使用equals()方法判断两个元素是否相等,contains()方法可以实现如下:

java
public class ArrayList {
    Object[] elementData;
    public boolean contains(Object o) {
        for (int i = 0; i < elementData.length; i++) {
            if (o.equals(elementData[i])) {
                return true;
            }
        }
        return false;
    }
}

因此,要正确使用Listcontains()indexOf()这些方法,放入的实例必须正确覆写equals()方法,否则,放进去的实例,查找不到。 eg:

java
public class Person {
    public String name;
    public int age;
    
    public boolean equals(Object o) {
        if (o instanceof Person p) {
            return Objects.equals(this.name, p.name) && this.age == p.age;
        }
        return false;
   }
    @Override // 要正确使用HashMap,作为key的类必须正确覆写equals()和hashCode()方法;
    int hashCode() {
        int h = 0;
        h = 31 * h + firstName.hashCode();
        h = 31 * h + lastName.hashCode();
        h = 31 * h + age;
        return h;
    }
}

IO

IO流是一种流式的数据输入/输出模型:

  • 二进制数据以byte为最小单位在InputStream/OutputStream中单向流动;

  • 字符数据以char为最小单位在Reader/Writer中单向流动。

Java标准库的java.io包提供了同步IO功能:

  • 字节流接口:InputStream/OutputStream;

  • 字符流接口:Reader/Writer。

Stream

一个无限自然数的stream

java
class NatualSupplier implements Supplier<Integer> {
    int n = 0;

    public Integer get() {
        n++;
        return n;
    }
}
public class Main {
    public static void main(String[] args) {
        Stream.generate(new NatualSupplier())
                .map((n) -> n * n)
                .limit(100)
                .forEach(System.out::println);
        
        // 计算1..100的和
        int sum = Stream.generate(new NatualSupplier())
                .limit(100)
                .reduce(0, Integer::sum);
        System.out.println("sum: " + sum);
    }
}

从一组给定的LocalDate中过滤掉工作日,以便得到休息日:

java
public class Main {
    public static void main(String[] args) {
        Stream.generate(new LocalDateSupplier())
                .limit(31)
                .filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY)
                .forEach(System.out::println);
    }
}

class LocalDateSupplier implements Supplier<LocalDate> {
    LocalDate start = LocalDate.of(2020, 1, 1);
    int n = -1;
    public LocalDate get() {
        n++;
        return start.plusDays(n);
    }
}

操作对Stream来说可以分为两类,一类是转换操作,即把一个Stream转换为另一个Stream,例如map()filter(),另一类是聚合操作,即对Stream的每个元素进行计算,得到一个确定的结果,例如reduce()

区分这两种操作是非常重要的,因为对于Stream来说,对其进行转换操作并不会触发任何计算

In case I don't see you. Good afternoon, good evening, and good night.