类是最基本的面向对象单元,是用于定义一组对象共同具有的状态和行为的模板。
类声明部分
一般的类声明格式为:[public ][abstract | final ][strictfp ]class 类名[ extends 父类名][ implements 父接口名]
访问限制符:public属于访问限制符。没有被访问限制符修饰的类只能在当前包内被调用。
public:被public修饰的类是一个公共类。可以被任意类或对象调用。每个Java源文件只能有一个public修饰的类。
abstract:被abstract修饰的类是一个抽象类。抽象类不能实例化,其中的抽象方法需要由子类重写。
final:被final修饰的类是一个最终类。最终类不能被继承,其中的方法不能被重写。
strictfp:被strictfp修饰的类是一个精确浮点类。该类中的所有代码都将严格地进行计算。严格约束意味着所有表达式的结果都必须是IEEE-754算法对操作数预期的结果,以单精度和双精度格式表示。
extends:继承其他类。每个类只能继承一个类。每个类默认继承Object类,省略该部分则表示继承Object类。
implements:实现其他接口。每个类可以实现多个接口。省略该部分则表示没有实现任何接口。
类体部分
类体部分包括了成员变量和方法的定义。
成员变量
成员变量也称为域,可以分为2种:
类变量:所有实例共有的变量,系统仅为类变量分配一次内存。类变量可以使用类名访问。
实例变量:每个实例特有的变量,每创建一个新实例都会为实例变量分配内存。实例变量只能使用对象名访问。
一般的成员变量声明格式为:[private | protected | public ][static ][final ][transient ][volatile ]类型 变量名
访问限制符:private、protected、public属于访问限制符。没有被访问限制符修饰的成员变量只能在当前包内被访问。
private:被private修饰的成员变量是一个私有变量,只能被当前类访问。
protected:被protected修饰的成员变量是一个保护变量,只能被当前类及其子类(同包或不同包)访问。
public:被public修饰的成员变量是一个公共变量,可以被任意类访问。
static:被static修饰的成员变量是一个类变量。没有被static修饰的成员变量是一个实例变量。
final:被final修饰的成员变量是一个最终变量(常量)。最终变量只能在声明时赋值,且不可再修改值。
transient:被transient修饰的成员变量是一个非永久变量。对象在序列化时不处理非永久变量。
volatile:被volatile修饰的变量可以被多个并行线程异步修改。
方法
方法可以分为2种:
类方法:所有实例共享的方法。类方法可以使用类名调用。
实例方法:每个实例特有的方法。实例方法只能使用对象名调用。
方法签名包括方法名和方法参数,是一个方法的唯一标识。一个类中不允许定义方法签名相同的两个方法。
一般的方法声明格式为:[private | protected | public ][static ][abstract | final ][native ][strictfp ][synchronized ]返回值类型 方法名(参数)[ throws 异常]
访问限制符:private、protected、public属于访问限制符。没有被访问限制符修饰的方法只能在当前包内被调用。
private:被private修饰的方法是一个私有方法,只能被当前类调用。
protected:被protected修饰的方法是一个保护方法,只能被当前类及其子类(同包或不同包)调用。
public:被public修饰的方法是一个公共方法,可以被任意类调用。
static:被static修饰的方法是一个类方法。没有被static修饰的方法是一个实例方法。
abstract:被abstract修饰的方法是一个抽象方法。抽象方法没有方法体,需要被子类重写。
final:被final修饰的方法是一个最终方法。最终方法不可以被子类重写。
native:被native修饰的方法没有方法体,由本地其他语言实现。
strictfp:被strictfp修饰的方法是一个精确浮点方法。精确浮点方法中所有的float和double表达式都严格遵守FP-strict的限制,符合IEEE-754规范。
synchronized:被synchronized修饰的方法一次只能由一个线程执行。
throws:抛出异常。每个方法可以抛出多种异常。抛出的异常将由调用该方法的类处理。
方法参数的参数名或方法体内的局部变量名可以与成员变量同名。此时如果直接使用变量名,根据就近原则,访问的是参数或局部变量,而不是成员变量。在这种情况下访问成员变量分为两种情况:
访问类变量:使用类名访问。
访问实例变量:使用this访问。this指代当前对象。
具体例子如下:
1 public class TestClass { 2 3 int i; // 实例变量 4 static float f; // 类变量 5 6 public void test1() { 7 int i = 10; // 局部变量 8 this.i = i; // 使用this.i访问实例变量i 9 float f = 10.0f; 10 TestClass.f = f; // 使用TestClass.f访问类变量f 11 } 12 13 public void test2(int i, float f) { // 方法参数 14 this.i = i; // 使用this.i访问实例变量i 15 TestClass.f = f; // 使用TestClass.f访问类变量f 16 } 17 18 }
构造方法
构造方法也称为构造器,是一种特殊的方法,用于创建新对象并对其初始化。通常情况下可以使用new运算调用构造方法对一个对象进行实例化和初始化。
一般的构造方法声明格式为:[private | protected | public ]类名(参数)
访问限制符:private、protected、public属于访问限制符。没有被访问限制符修饰的方法只能在当前包内被调用。
private:被private修饰的构造方法是一个私有方法,只能被当前类调用。一般不使用private修饰构造方法。
protected:被protected修饰的构造方法是一个保护方法,只能被当前类及其子类(同包或不同包)调用。
public:被public修饰的构造方法是一个公共方法,可以被任意类调用。
构造方法与一般方法的区别有:
1.构造方法的方法名只能为类名。
2.构造方法不能声明返回值类型。
3.构造方法的修饰符只有访问限制符。
若无在类体部分显式地定义构造方法,默认会提供一个公共的空构造方法。空构造方法没有参数,方法体没有内容。
代码块
使用“{ }”括起来的多行代码称为一个代码块。在代码块中声明的变量只能在该代码块中使用。
代码块分为3种:
局部代码块:定义在方法体中的代码块。
构造代码块:定义在类中的代码块。每一次创建新实例时会执行构造代码块。
静态代码块:使用static修饰的定义在类中的代码块。第一次创建新实例或使用类成员时会执行静态代码块。
第一次创建实例时,代码块的执行顺序是:静态代码块->构造代码块->构造方法;之后每一次创建实例时,代码块的执行顺序是:构造代码块->构造方法。
例:A类有一个无参构造方法、构造代码块和静态代码块。
1 class A { 2 3 A() { 4 System.out.println("构造方法......"); 5 } 6 7 // 构造代码块 8 { 9 System.out.println("构造代码块......"); 10 } 11 12 // 静态代码块 13 static { 14 System.out.println("静态代码块......"); 15 } 16 17 }
1 @Test 2 void test() { 3 System.out.println("--第一次创建实例--"); 4 new A(); 5 System.out.println("--第二次创建实例--"); 6 new A(); 7 }
运行结果:
若一个类继承了其他类,则第一次创建该类的实例时,代码块的执行顺序是:父类静态代码块->子类静态代码块->父类构造代码块->父类构造方法->子类构造代码块->子类构造方法;之后每一次创建实例时,代码块的执行顺序是:父类构造代码块->父类构造方法->子类构造代码块->子类构造方法。
例:A类有一个无参构造方法、构造代码块和静态代码块。B类继承了A类,有一个无参构造方法、构造代码块和静态代码块。
1 class A { 2 3 A() { 4 System.out.println("父类构造方法......"); 5 } 6 7 // 构造代码块 8 { 9 System.out.println("父类构造代码块......"); 10 } 11 12 // 静态代码块 13 static { 14 System.out.println("父类静态代码块......"); 15 } 16 17 } 18 19 class B extends A { 20 21 B() { 22 System.out.println("子类构造方法......"); 23 } 24 25 // 构造代码块 26 { 27 System.out.println("子类构造代码块......"); 28 } 29 30 // 静态代码块 31 static { 32 System.out.println("子类静态代码块......"); 33 } 34 35 }
1 @Test 2 void test() { 3 System.out.println("--第一次创建实例--"); 4 new B(); 5 System.out.println("--第二次创建实例--"); 6 new B(); 7 }
运行结果:
抽象类
抽象类是通过abstract修饰的类。
抽象类与一般类的区别有:
1.抽象类的修饰符不能有final或strictfp。
2.抽象类中可以定义抽象方法。
3.抽象类不能直接使用new运算实例化。需要有一个子类继承该抽象类并重写抽象方法,然后使用new运算调用子类构造方法实例化。
抽象方法是通过abstract修饰的方法。抽象方法没有方法体,需要由继承该抽象类的子类重写。
嵌套类
一个类可以在另一个类的类体部分定义。使用嵌套类可以提高代码的可读性和可维护性,增强封装性,并增加类之间的逻辑联系。
静态嵌套类:静态嵌套类内可以定义类成员,只能访问或调用外部类的类成员。
1 class TestClass { 2 3 static float f; // 类变量 4 5 static void method1() { // 类方法 6 7 } 8 9 // 静态嵌套类 10 static class StaticNestedClass{ 11 12 void test() { 13 float f1 = f; // 访问类变量 14 method1(); // 调用类方法 15 } 16 17 } 18 19 }
内部类:内部类内定义的成员只能定义实例成员,可以访问或调用外部类的任何成员。内部类对象的实例化需要通过外部类对象使用new运算。
1 class TestClass { 2 3 int i; // 实例变量 4 static float f; // 类变量 5 6 static void method1() { // 类方法 7 8 } 9 10 void method2() { // 实例方法 11 12 } 13 14 // 内部类 15 class InnerClass { 16 17 void test() { 18 int i1 = i; // 访问类变量 19 float f1 = f; // 访问实例变量 20 method1(); // 调用类方法 21 method2(); // 调用实例方法 22 } 23 24 } 25 26 }
局部内部类:局部内部类是一种定义在方法体内的内部类,只能在当前方法中被调用。局部内部类只能使用abstract或final修饰,且不能继承其他类和实现其他接口。局部内部类无法访问或调用外部类的成员。
1 class TestClass { 2 3 void test() { 4 // 局部内部类 5 class LocalInnerClass { 6 7 } 8 } 9 10 }
匿名内部类:匿名内部类是一种特殊的局部内部类,只用于一次实例化和初始化。匿名内部类无法使用修饰符修饰,且只能在new运算时定义。new运算是对对象的实例化和初始化,所以匿名内部类相当于是继承该对象对应的类或实现该对象对应的接口。
1 class TestClass { 2 3 abstract class A { 4 abstract void test(); 5 } 6 7 interface B { 8 void test(); 9 } 10 11 void test() { 12 // 匿名内部类,相当于继承抽象类A 13 A a = new A() { 14 15 @Override 16 void test() { 17 18 } 19 20 }; 21 // 匿名内部类,相当于实现接口B 22 B b = new B() { 23 24 @Override 25 public void test() { 26 27 } 28 29 }; 30 } 31 32 }
一般的嵌套类声明格式为:[private | protected | public ][static ][abstract | final ][strictfp ]class 类名[ extends 父类名][ implements 父接口名]
访问限制符:private、protected、public属于访问限制符。没有被访问限制符修饰的嵌套类只能在当前包内被调用。
private:被private修饰的嵌套类是一个私有类,只能被当前类调用。
protected:被protected修饰的嵌套类是一个保护类,只能被当前类及其子类(同包或不同包)调用。
public:被public修饰的嵌套类是一个公共类,可以被任意类调用。
static:被static修饰的嵌套类是一个静态嵌套类。没有被static修饰的嵌套类是一个内部类。
abstract:被abstract修饰的嵌套类是一个抽象类。抽象类不能实例化,其中的抽象方法需要由子类重写。
final:被final修饰的嵌套类是一个最终类。最终类不能被继承,其中的方法不能被重写。
strictfp:被strictfp修饰的嵌套类是一个精确浮点类。该类中的所有代码都将严格地进行计算。严格约束意味着所有表达式的结果都必须是IEEE-754算法对操作数预期的结果,以单精度和双精度格式表示。
extends:继承其他类。每个嵌套类只能继承一个类。每个类默认继承Object类,省略该部分则表示继承Object类。
implements:实现其他接口。每个嵌套类可以实现多个接口。省略该部分则表示没有实现任何接口。
嵌套类与一般类的区别有:
1.嵌套类额外多出private、protected、static三个修饰符。
2.嵌套类定义在外部类的类体部分,所以嵌套类的全类名是:包名.外部类名.类名。
类的继承和实现
一个类的类声明部分如果使用了extends子句,则表示该类继承了其他的类;如果使用了implements子句,则表示该类实现了其他的接口。此时,当前类称为子类,继承的类称为父类,实现的接口称为父接口。
Java不支持多重继承,即只允许继承一个类。类声明时如果没有使用extends子句,则表示继承了Object类。Java允许实现多个接口,来代替多重继承。
子类对象的类型可以转换为父类或父接口类型,而父类对象的类型不一定可以转换为子类类型。
一个类继承另一个类时,需要重写其抽象方法。如果父类没有定义无参构造方法时,则子类的构造方法需要使用super关键字调用父类的构造方法。
优点:
1.子类拥有父类的所有方法和属性,从而可以减少类体定义的工作量。
2.提高了代码的重用性。
3.提高了代码的扩展性,子类不但拥有了父类的所有功能,还可以添加自己的功能。
缺点:
1.继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
2.降低了代码的灵活性。因为继承时,父类会对子类有一种约束。
3.增强了耦合性。当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。有时修改了一点点代码都有可能需要对打断程序进行重构。
里氏代换原则
里氏代换原则(Liskov Substitution Principle,LSP)是衡量两个类之间是否满足继承关系的一个标准,指任何父类能够出现的地方,子类都能够出现。在实际代码环境中,如果一个类的对象直接用另一个类的对象代替,运行结果没有区别,那么说明这两个类之间符合里氏代换原则,满足继承关系。
例如:定义一个Rectangle类和一个Square类,其中Square类继承了Rectangle类。
1 class Rectangle { 2 3 int width; 4 int height; 5 6 public void setWidth(int width) { 7 this.width = width; 8 } 9 10 public void setHeight(int height) { 11 this.height = height; 12 } 13 14 @Override 15 public String toString() { 16 return "width=" + width + ", height=" + height; 17 } 18 19 }
1 class Square extends Rectangle { 2 3 @Override 4 public void setWidth(int width) { 5 this.width = width; 6 height = width; 7 } 8 9 @Override 10 public void setHeight(int height) { 11 this.height = height; 12 width = height; 13 } 14 15 }
如果此时有这么一个方法:
1 public static Rectangle test(Rectangle rectangle) { 2 while (rectangle.width <= rectangle.height) 3 rectangle.setWidth(rectangle.height + 1); 4 return rectangle; 5 }
可以发现,传入Rectangle对象时可以正常运行,而传入Square对象就会陷入死循环。即这两个类不符合里氏代换原则,不应该使用继承关系。
里氏代换原则对Java继承和实现的约束主要体现为以下方面:
1.子类只重写父类或父接口的抽象方法,不重写其非抽象方法。
2.子类重载父类或父接口的方法时,参数的类型不能为父类或父接口方法的参数类型及其子类型。