1. Java的访问控制权限
1.1 访问控制权限总体描述
-
Java 中有三个访问权限修饰符:
private
、protected
以及public
,如果不加访问修饰符,表示包级可见。有资料将其称为default
。 -
级别从高到低排序:
public
、protected
、default
、private
,他们可以修饰类、类中的成员变量和方法。 -
访问控制权限总结如下:
访问控制权限 同一个类 同一个包 不同包中的子类 其他包中的类 public Yes Yes Yes Yes protected Yes Yes Yes default Yes Yes private Yes -
public:
公共访问权限
,如果一个类中的成员或方法使用了public访问权限,就可以在所有类中被访问。不管是否在同一个包中。 -
protected:
继承访问权限
,修饰父类的访问控制权限
,如果父类和子类不属于同一个包,想要继承父类拥有的成员但又不想设置为 public 访问权限,则可以设置为 protected 访问权限。同一个包中的类都可以访问,不同包的子类也可以访问。
-
default:
包访问权限
,即不添加访问修饰符的时候 ,表示同一个包中可访问。 -
private:
私有访问权限
,被修饰的属性和方法只能被所在的类访问
,其他任何类都无法访问。应用场景:
单例模式,其中的属性和方法都是private
权限。
自己的心得:
- 在创建父类时,如果不想设置父类的属性或方法为public权限,又需要创建不同包中的子类,这时候可以使用protected。
- 设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为
信息隐藏或封装
。因此访问权限应当尽可能地使每个类或者成员不被外界访问
。 - 如果子类的方法重写了父类的方法,那么
子类中该方法的访问级别不允许低于父类的访问级别
。这是为了确保可以使用父类实例的地方都可以使用子类实例
,也就是确保满足里氏替换原则
。如父类方法的访问权限为protected,子类方法应该为protected或public。 属性最好不能是公有的
,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。最好设置为private, 提供getter和setter方法,实现属性的修改。
1.2 类的访问控制权限
- 上面介绍的都是类的属性或方法的访问控制权限,外部类的访问控制权限
只能使用public或default
;内部类四种权限都可以。这时,内部类就像外部类的一个成员。
类的定义还有如下要求:
- 每个 java 文件只能有一个 public class。 比如leetcode刷题时,带main函数的类是public类,而我们需要额外创建数据结构
TreeNode
要放在同一个Java文件中的话,只能是default权限。 - public class 的名称和 java 文件的名称必须保持一致。 比如我创建的java文件名为Test,只能有
public class Test
,定义public class Hello
会报错。 - java 文件中可以没有
public class
,所有的class 都是default权限。这时java文件名可以随意
,不一定得跟class的名字一样。这种方式降低代码的可读性和可维护性,不建议使用!
2. Java构造函数
2.1 构造函数的类型
两种类型的构造函数:
- 无参构造函数(默认构造函数): 没有参数的构造函数称为默认构造函数。 如果我们没有在类中定义构造函数,编译器会为该类创建一个默认构造函数。为了区别我们自己定义的无参构造函数和编译器创建的无参构造函数,我习惯将后者称为默认构造函数。
- 有参构造函数: 具有已知参数的构造函数是有参构造函数。 如果我们想在创建对象时,就传入相应的值对某些属性进行初始化,就可以使用有参构造函数。
-
具体例子:
public class Test2 { private String name; public Test2(){ System.out.println("我是无参构造函数"); } public Test2(String name){ System.out.println("我是有参构造函数"); this.name=name; } }
2.2 构造函数的特性
- 构造函数是一种特殊的函数,用来在类实例化时初始化类对象的成员变量,即在构造函数中完成对象的初始化工作。
构造函数特性:
- 构造函数名与类名一致,并且不能有返回值。注意: 不能有返回值是指连void也不可以!
- 可以有多个构造函数。如果开发人员没有提供构造函数时,编译器在把源代码编译成字节码的过程中会提供一个默认构造函数,但该构造函数不会执行任何代码。 如果开发人员提供了构造函数,编译器就不会再创建默认构造函数。
- 构造函数的参数可以为任意数,如0个、1个或1个以上的参数。为0个时,就是无参构造函数,即默认构造函数。
- 构造函数由JVM调用,总是伴随着new操作一起调用。
- 构造函数在类实例化时会被自动调用,且只运行一次。而普通方法是在程序执行到它时被调用,且可以被调用多次。
- 构造函数可以重载不能重写,即可以使用参数个数、类型、顺序至少有一个不同。
- 注意: 一旦定义了有参构造函数,系统不再为我们构造默认构造函数,需要我们自定义无参构造函数。这时
Test test=new Test()
会报错,因为找不到无参构造函数!
2.3 子类如何访问父类的构造函数?
-
子类通过super关键字显式地使用父类构造函数,super关键字语句必须是子类构造函数的第一个语句。
-
如果子类重写了父类的普通方法,也可以通过使用 super 关键字显式地使用父类方法。super关键字语句不要求是子类方法的第一个语句。
public SuperExtendExample(int x, int y, int z) {// 子类的构造函数 super(x, y);// 显式调用父类的有参构造函数 this.z = z; } @Override public void func() {// 子类重写类父类的func() super.func();// 系那是调用父类的func() System.out.println("SuperExtendExample.func()"); }
关于子类和父类的构造方法,有以下需要注意的地方:
-
子类只继承父类的无参构造函数,如果
父类没有无参构造函数
,则子类不能从父类继承无参构造函数。父类没有无参构造函数的情况: 父类定义了有参构造函数,没有定义无参构造函数。 -
在创建对象时,先执行父类构造函数,再执行子类自身的构造函数。
-
如果父类提供了无参构造函数,则子类的构造函数就可以不用显式地调用父类的构造函数。在这种情况下,编译器会默认调用父类提供的无参构造函数。
(1)下面的代码不会提示错误,因为父类有无参构造函数。
(2)有无参构函数的情况: ① 父类没有定义构造函数,编译器自动为其生成无参构造函数;② 父类同时定义了有参和无参构造函数public Child(String name) { this.name = name; }
-
当父类没有提供无参构造函数,却定义了有参构造函数,则子类的构造函数中必须显式地调用父类的有参构造函数。上面的情况会提示:
There is no default constructor available in Father
-
默认构造函数的修饰符只跟当前类的修饰符有关,如果class为default权限,则默认构造函数也是default。当然,我们可以显示指定构函数的权限为private或protected:如果为private,会提示你
Private constructor 'Test()' is never used
。如果为protected,只有子类才能访问(貌似子类必须是同一个包。明明父类是public?)
普通方法是否可以与构造函数有相同的方法名?
- 可以,只是普通方法必须具有返回值
2.4 Java类的初始化顺序
不存在继承关系的情况下
- 静态变量、静态语句块(优先级并列,按出现的先后顺序执行)(只在第一次加载类时执行)
- 实例变量、普通语句块(优先级并列,按出现的先后顺序执行)
- 构造函数
public class InitialOrderTest {
/* 静态变量 */
public static String staticField = "静态变量";
/* 实例变量 */
public String field = "实例变量";
/* 静态初始化块 */
static {
System.out.println( staticField );
System.out.println( "静态初始化块" );
}
/* 普通语句块 */
{
System.out.println( field );
System.out.println( "普通语句块" );
}
/* 构造函数 */
public InitialOrderTest()
{
System.out.println( "构造函数" );
}
public static void main( String[] args )
{
new InitialOrderTest();
}
}
- 以上代码的输出:
静态变量-> 静态初始化块->实例变量-> 普通语句块-> 构造函数
- 存在继承关系时,类初始化顺序为
- 父类的静态变量、静态语句块(优先级并列,按出现的先后顺序执行)(只在第一次加载类时执行)
- 子类的静态变量、静态语句块(优先级并列,按出现的先后顺序执行)(只在第一次加载类时执行)
- 父类的实例变量、普通语句块(优先级并列,按出现的先后顺序执行)
- 父类的构造函数
- 子类的实例变量、普通语句块(优先级并列,按出现的先后顺序执行)
- 子类的构造函数
- 存在继承关系的具体代码见博客:Java类初始化顺序
3. 重写与重载
3.1 重写(覆盖,Override)
- 重写: 父类与子类之间的多态性,对父类的函数进行重新定义。子类中定义到的方法与父类方法,方法名和参数列表都相同,则称为子类重写或覆盖 (Override)了父类的方法。
- 为什么又叫覆盖? 当子类重写了父类的方法时,子类调用该方法时,系统将会自行调用子类的该方法而不是父类的该方法。可以说子类的该方法覆盖了父类的该方法。
- 如果子类重写了父类的方法,而子类又想调用父类的该方法,则可以
使用super关键字
。
重写的规则:
- 子类方法的方法名、参数列表必须与父类原方法相同。
- 子类方法的访问权限一定要
大于等于
父类方法权限(public > protected > default> private
)。比如,如果父类方法的访问权限为protected,则子类方法可以为protected或public。 - 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。如子类抛出的异常类型为 Exception,是父类抛出异常 Throwable 的子类。
- 子类方法的返回类型必须是父类方法返回类型或为其子类型。如子类的返回类型为 ArrayList,是父类返回类型 List 的子类。
- 被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
3.2 重载(Overload)
-
重载: 存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
-
注意: 参数的顺序不同,要求参数类型必须不一样。以下情况会报错:
public void hello(int a, int b) { System.out.println(a + "," + b); } public void hello(int b,int a){ System.out.println(a + ";" + b); }
-
重载是一个类中多态性的一种表现: 类中的多个方法具有相同的名字,但具有不同的参数列表。调用方法时通过传递给他们的参数个数和参数类型来决定具体使用哪个方法, 这就是多态性。
重载的规则:
- 重载要求方法名相同、参数列表不同。参数列表不同包括:参数的个数、参数的类型、参数的顺序(参数类型必须不一样),这三种情况的任意一种即可。
- 访问权限或者返回值或者抛出的异常不同,其它都相同不算是重载。
3.3 重写与重载的总结
从重载与重写的要求来看:
- 重写是子类方法与父类方法之间的关系,重载是同一个类中方法之间的关系。
- 重写要求方法名和参数列表都相同,重载要求方法名相同、参数列表不同。
- 重写要求子类方法访问权限大于等于父类的、返回值、抛出的异常必须是父类类型或其子类型;重载中,访问权限、返回值、抛出的异常不同,其他相同也不是重载。
从多态性来看
- 重写是子类与父类之间多态性的一种体现,重载是同一个类中多态性的体现。
- 重载是一个编译期概念、重写是一个运行期间概念。
- 重载遵循所谓
编译期绑定
,即在编译时根据参数变量的个数和类型判断应该调用哪个方法
。 - 重写遵循所谓
运行期绑定
,即在运行是根据引用变量所指向的实际对象类型来调用方法
- 严格来说,重载是编译时多态,即静态多态。重写是运行时多态,即动态多态,正是
java中通常所说的多态
。