方法 | 函数
递归函数
在方法体内调用方法本身,方法要有出口。
构造方法
如果没有显式提供构造方法,会自动提供一个无参的构造方法作为默认的构造方法;如果显式提供了构造方法,则不再自动提供默认的构造方法。
如果子类显式提供了构造方法
- 如果显式调用了父类的构造方法:则调用语句必须是函数体中的第一个语句
- 如果没有显式调用父类的构造方法:会自动在函数体的首行调用父类的无参构造方法,如果父类没有提供无参的构造方法,则编译报错
不能直接使用 父类名() 的方式调用父类的构造方法,要通过 super() 来调用,super() 只能在构造方法中使用。
可以在实例方法中通过super对象可以访问父类的成员,super对象属于实例、不是类的公共属性,不能在静态方法中使用super对象。
函数重载
函数重载、重写的区别
- 重载 overload:发生在同一个类中,方法名相同,但参数类型或个数不同,与返回值类型、访问修饰符无关。
- 重写 override:发生在子类中,修改、覆盖父类方法的实现,只能重写被继承的方法(访问权限不为private),方法名、形参表都必须相同,返回值类型要属于(小于等于)父类方法的返回值类型,抛出的异常要属于(小于等于)父类方法抛出异常,方法访问权限要大于等于父类
构造方法不能被重写,但可以被重载。
参数个数可变
放在形参表末尾,调用时该位置可传入0个或多个指定类型的参数,实质是以数组方式传入,在函数体中以数组方式进行操作。
public void test(String str, String... args) {
//jvm会预先创建一个数组来接收个数可变的参数,即使传了0个参数,args也不为null
//可根据数组的元素个数来判断是否传入了参数
if(args.length!=0){
for (String arg : args) {
//...
}
}
}
- 传参方式:java只有一种传参方式——传值,把实参的值拷贝一份传给形参。拷贝使用的是浅拷贝,基本数据类型、String拷贝的是其中的数值常量、字符串常量,修改形参不会影响到实参;引用类型拷贝的是对象引用,形参、实参指向同一个对象,修改形参就是修改实参。
Nullable、Nonnull 提示
可以在形参前面、方法上面标注
- @Nullable:告诉编译器可以为null
- @Nonnull:告诉编译器不能为null,标注在方法上面时,表示方法返回值不会为null;标注再形参前面时,表示该参数不能传null。
这2个注解推荐使用 javax、spring 提供的,javax需要引入 jsr305,这个依赖 guava 之类常见的框架中已经包含了。有的公司引入的是 jetbrains.annotations,使用 @Nullable、@NotNull。
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>16.0.1</version>
</dependency>
类
面向对象的3大特性
继承
构造方法
- 父类的构造方法不管被哪种访问权限修饰,都不会被子类继承;
- 子类构造方法必须在方法体的首行调用父类的构造方法,可以
super(xxx)
显式调用父类的指定构造方法,也可以缺省,缺省时编译器会自动在首行加上super()
调用父类无参的构造方法。
重写父类的方法
- 方法名相同、形参表相同
- 如果父类方法是类方法(static)| 实例方法,子类方法也要是static | 实例方法,与父类保持一致
- 子类方法的返回值类型要 <= 父类方法的返回值类型
- 子类方法抛出的异常类型要 <= 父类方法抛出的异常类型
- 子类方法的访问权限要 >= 父类方法的访问权限(如果父类方法使用private修饰,则子类不会继承该方法,也就不能重写)
在子类中访问父类的成员
super(xxx) //调用父类的构造函数
super.xxx //访问父类的成员变量(要有权限才可以)
super.xxx(xxx) //调用父类的成员方法
创建对象时,会先初始化这个类所有的父类。
多态
引用型变量有2个类型:编译时类型、运行时类型。
编译时类型是该变量声明的类型,运行时类型是该变量实际的类型。
多态:编译时类型相同,但运行时类型可以不一致,可以是不同的子类。
public void f(User user) {
}
编译时类型是User,假设User有2个子类Student、Teacher,运行时可以传入Student、Teacher类的对象,类型不一致,即多态。
判断某个引用型变量是否是某个类 | 接口的实例
User user = new User();
if(user instanceof User){ //返回boolean值
}
变量 instanceof 类 | 接口名,变量要声明为该类|接口类型,否则通不过编译。
内部类
内部类:类中嵌套其它类的定义
匿名内部类:没有类名的内部类,只能使用一次,可以用来实现接口、抽象类,会在该处创建匿名内部类的一个实例,可以用new来写实现。
静态内部类:使用static修饰的内部类,可以通过 外部类.内部类 的方式进行访问。
抽象类
- 抽象类本身使用abstract修饰,不能被实例化,是用来被继承、重写的。
- 抽象方法使用abstract修饰,没有方法体。
- 抽象类中可以不含抽象方法,但如果类中有抽象方法,该类必须声明为抽象类。
枚举
枚举常用于表示同一种类的常量集合,比如订单状态、用户角色等。
以前的方式
public class Role {
public static final Integer COMMON = 1;
public static final Integer VIP = 2;
public static final Integer SVIP = 3;
//...
}
枚举
- 提供 全参构造器、getter方法,全参构造器最好定义为 priavte 对外隐藏;
- 枚举实例要放在成员变量之前,不然会报错。
@AllArgsConstructor //手写改为 private 更好
@Getter
public enum RoleEnum {
//实质是调用构造函数创建当前类的实例,作为类的静态成员
COMMON(1),
VIP(2),
SVIP(3),
;
private Integer code;
}
可以不要成员变量
public enum RoleEnum {
COMMON, VIP, SVIP;
}
枚举与switch
/**
* 根据code获取对应的枚举实例
*
* @param code
* @return
*/
public static RoleEnum getEnumByCode(Integer code) {
if (Objects.isNull(code)) {
return null;
}
for (RoleEnum roleEnum : RoleEnum.values()) {
if (roleEnum.getCode().equals(code)) {
return roleEnum;
}
}
return null;
}
不建议返回null,可能导致NPE,推荐设置一个 OTHER 之类的枚举实例,没有匹配的枚举实例时返回该实例,避免后续的判空、NPE。
switch (RoleEnum.getEnumByCode(roleCode)) {
case COMMON:
//...
break;
case VIP:
//...
break;
case SVIP:
//...
break;
default:
}
switch() 的参数不能为null,否则会报NPE
说明
- 枚举会自动继承抽象类Enum,java中的类只支持单继承,所以枚举不能再继承其它类。
- 编译时会把枚举替换成一个对应的类,且使用final修饰,不能被继承。枚举实例会以类的实例的形式做为类的静态成员,使用static final修饰。
- 枚举实例之间的判断,用 ==、equals() 均可
RoleEnum roleEnum1 = RoleEnum.COMMON;
RoleEnum roleEnum2 = RoleEnum.COMMON;
System.out.println(RoleEnum.COMMON == roleEnum1);
System.out.println(RoleEnum.COMMON.equals(roleEnum1));
System.out.println(roleEnum1 == roleEnum2);
System.out.println(roleEnum1.equals(roleEnum2));
均为 true
接口
- 类是对实例的抽象,抽象类、接口是对类的抽象。
- 接口定义程序设计规范,本身并不提供任何实现,体现了规范(设计)、实现分离的设计方式。
public interface Xxx {
// 成员变量默认使用public static final修饰,即常量,在声明时就必须指定初始值
String XXX = "";
// 方法默认使用public abstract修饰
String xxx();
// 可以定义默认方法,但必须要有方法体
default void test2() {
}
//接口中可以定义静态方法方法,但必须要有方法体,默认使用(也只能使用)public修饰
static void test3() {
}
// 内部也可以定义类、接口、接口,均默认使用(也只能使用)public static修饰
// 不能有构造函数、初始化块、私有方法
}
// 类只支持单继承,接口支持多继承
class 类A extends 类B {
}
interface 接口A extends 接口B, 接口C {
}
// 一个类可以实现多个接口
class 类A implements 接口A, 接口B {
}
// 类在继承的同时可以实现接口
class 类A extends 类B implements 接口A, 接口B {
}
接口、抽象类的区别
- 接口中只能定义常量、方法,不能表达对象状态的变化(常量不可变);抽象类中可以定义对象的成员字段,可以表达对象状态的变化。
- 接口中成员的访问权限默认为且只能为 public,方法可以abstract、default、static之一修饰,默认使用 abstract 修饰,抽象类则无此限制。
- 类可以同时实现多个接口,但不能同时继承多个类。
一般把子类共有的成员变量、实现相同的公共方法抽象出来放在抽象类中,实现不同的公共方法也可以作为抽象方法放在抽象类中;子类中公共的常量,以及实现不同的公共方法,可以放在接口中。
泛型
泛型可以限制对象|元素类型,在编译时对数据类型进行校验。
//在普通的成员变量中使用泛型
public class Test<T> {
private T t;
public Test(T t) {
this.t = t;
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
//静态成员字段不能使用泛型,但静态方法的返回值类型、参数类型可以使用泛型
class Test {
//需要在返回值类型前用< >进行声明
public static <T> T test(T t) {
return t;
}
}
//类名后面标注的泛型可以用extend指定上边界,但不能用super指定下边界
public class Test<T extends Number> {
}
//可以使用多个泛型
public class Test<T extends Number, E> {
}
public class Test<T> {
//使用泛型声明变量、返回值类型时,可以用? extend指定上边界,必须要是Number的子类
public Test<? extends Number> handler1(Test<? extends Number> test) {
return test;
}
//可以用? super声明下边界,必须要是Integer的父类
public void handler2(Test<? super Integer> test) {
}
//上边界、下边界都包含边界本身
}
注意点
- 泛型不能使用int、float等基本类型
- 编译时会擦除泛型,生成的字节码中没有泛型。
- 对于同一个类,不管使用哪种泛型,都是同一个类,getClass()获取到的都是同一个Class对象,实例都属于同一个类。
- 静态成员变量不能使用泛型,是因为类加载时会初始化静态成员,如果使用泛型则不能确定数据类型,无法赋初值。
lambda表达式
函数式接口:接口中的抽象方法有且只有1个。可以有其它成员,但抽象方法必须要有且只能有1个。
lambda表达式是函数式接口的匿名内部类实现的语法糖,可以代替函数式接口的匿名内部类,只能代替函数式接口的匿名内部类,不能代替其它的匿名内部类。
@FunctionalInterface //声明为函数式接口
interface FI {
void out(String str1,String str2);
}
class T{
// lambda表达式
private FI fi = (str,str2) -> { //参数表直接写参数名,不写类型
System.out.println(str + " " + str2);
};
}
lambda表达式用于实现函数式接口、并在该处创建一个实例,和匿名内部类相似,但匿名内部类可以实现任何抽象类、接口,lambda表达式只能实现函数式接口。
lambda表达式语法
(形参表) -> { //形参表不写参数类型,直接写参数名
//......
}
lambda表达式其实就是函数式接口中抽象方法的实现。
只有一个形参时,可以缺省();函数体只有一条语句,且该语句是return语句时,可以缺省{ }、关键字return。