1、Java中的构造函数
1.1、系统默认生成的构造方法
构造方法的名称就是类名,构造方法的参数没有限制,在方法内部,也可以编写任意语句。和普通方法相比,构造方法没有返回值(也没有void
),调用构造方法,必须用new
操作符。
Java 中的任何类 class 都有构造方法,如果在一个类没有手动定义构造方法,编译器会自动为我们生成一个默认构造方法,它既没有参数,也没有执行语句。类似于下面的代码:
系统生成的默认的构造方法的访问修饰符和该类的访问修饰符一致。
public class Person { public Person() { } } class Person2 { Person() { } }
1.2、定义多个构造方法
如果我们自定义了一个构造方法,那么,编译器将不会自动创建默认构造方法。如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来。否则如果没有手动定义不带参数的构造函数,而调用不带参数的构造函数来创建实例的话就会编译错误,因为编译器找不到这个构造方法。
可以定义多个构造函数,并且可以分别调用多种形式的构造函数来创建实例。一个构造方法可以通过 this(aaa, bbb) 的形式来调用其他构造方法,这有利于代码复用,不过该语句必须放在第一行。
public class Main { public static void main(String[] args) { Person p1 = new Person("Xiao Ming"); // 既可以调用带参数的构造方法 Person p2 = new Person(); // 也可以调用无参数构造方法 } } class Person { private String name; private int age; public Person() { } public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); //这条语句必须放在第一行,否则报错 this.age = age; } }
2、继承(extends)
在 Java 中使用 extends 关键字来实现继承,在继承之后,子类就拥有了父类中的方法和字段,只需要往子类中添加额外的字段和方法即可(extends,即子类不是父类的子集,而是父类的扩展)。多个类中存在相同的属性和方法时,我们就可以将这些共同的属性和方法抽取到一个单独的类中,其它类就可以不必再定义这些属性和方法,只要继承那个类即可。
class Person { private String name; private int age; public String getName() {...} public void setName(String name) {...} } class Student extends Person { // 不要重复name字段/方法, // 只需要定义新增score字段/方法: private int score; public int getScore() { … } public void setScore(int score) { … } }
继承树示例:
在面向对象编程(OOP)的术语中,我们把 person 称为超类、父类或者基类,把 student 称为子类或者扩展类。
我们在定义一个父类的时候不需要写extends,在Java中,没有写extends
的类,编译器会自动加上extends Object,即继承自 Object。所以,任何类,除了 Object 都会继承自某个类。
Java中一个class只允许继承自一个类,因此,一个类有且仅有一个父类。只有Object
特殊,它没有父类。
2.1、继承的作用
1)提高了代码的复用性
2)继承的出现让类与类之间产生了关系,提供了多态的前提
3)不要仅为了其它类中的某个功能而去继承 。应该只有两者间有逻辑的一致性时才考虑用继承
2.2、super 关键字(表示父类)
super 关键字表示父类(超类),子类引用父类的字段时,可以用 super.filedName()。如果子类没有定义该字段,那么使用 super.fieldName、this.fieldName 或者直接调用 fieldName 都是一样的效果,编译器会自动定位到父类的该字段。super 代表的不一定是直接父类的变量或者方法,也可能是父类的父类的变量或者方法,如果直接父类没有该方法或者变量时,以此类推,可以一直向上追溯。
任何 class 的构造方法,第一行语句必须是调用父类的构造方法。如果在子类的构造方法中没有明确地调用父类的构造方法,编译器会帮我们自动加一句 super() 来调用父类的构造方法。但是,如果父类中没有无参数的构造方法时,我们就必须手动在子类的构造函数中使用 super(aaa, bbb) 来调用父类的有参构造方法,否则会编译失败。
public class Person{ public Person(String name) { System.out.println("demo003"); } } public class Student extends Demo03{ public Student() { super("zhangsan"); //父类没有无参的构造方法,此时必须显式调用父类的构造方法 } }
因此我们得出结论:如果父类没有默认的构造方法,子类就必须显式地写出自己的构造函数,并且调用 super() 同时给出参数以便让编译器定位到父类的一个合适的构造方法。
子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
2.3、向上转型(允许)
向上转型:把一个子类类型的值安全地变为父类类型的赋值,即将一个子类类型的实例赋值给一个父类类型的变量。子类可以看做是一个特殊的父类。
向上转型后,该父类类型的变量不能访问子类中额外添加的属性和方法。因为属性是在编译时确定的,编译时该变量为父类类型,所以就没有子类中额外添加的属性和方法。
如果访问子类中已经覆写了的方法,那么实际上调用的也是子类中的方法,因为方法的调用是在运行时决定的。如果访问的是属性,即使子类中也有相同的属性,但访问的仍然是父类的属性。
class Person { private int age; public String name = "person"; public void setAge(int age) { this.age = age; } public void run() { System.out.println("Person.run"); } } class Student extends Person { private int score; public String name = "student"; public void setScore(int score) { this.score = score; } @Override public void run() { System.out.println("Student.run"); } } Person p = new Student(); // Student 继承自 Person p.setAge(22); //可以对 p 进行任何父类的操作 p.run(); //打印 Student.run。如果子类覆写了父类的方法,那么实际上执行的是子类的方法 System.out.println(p.name); //输出person。属性的话都是输出父类的属性 p.setScore(99); //报错。不能对 p 进行只属于子类 student 的操作,会报编译错误
在 Java 中,这种指向完全是允许的。注意,只能进行父类的操作,如果进行只属于子类的操作的话就会报编译错误。
向上转型实际上是把一个子类型安全地变为更加抽象的父类型:
Student s = new Student(); Person p = s; // upcasting, ok Object o1 = p; // upcasting, ok Object o2 = s; // upcasting, ok
2.4、向下转型(特定情况允许)
向下转型:把一个父类类型的值强制转型为子类类型。只有该父类变量原本就是通过向上转型而来的才可以成功。
将某一父类的变量赋值给子类变量,如果该父类变量原本就是指向子类的话,那么运行可以成功,否则运行将报错。因为,向下转型只有在该父类变量的值原本就是指向子类实例的情况下才可以运行。
Person p1 = new Student(); Person p2 = new Person(); Student s1 = (Student) p1; // p1实际上指向的就是student实例,所以可以成功 Student s2 = (Student) p2; // p2实际上指向的是person实例,所以这里运行会报错 ClassCastException!
注意,在转型前必须先进行强制类型转换,否则编译报错。
Person p1 = new Student(); Student s1 = p1; // 没有强制转换,编译直接报错
2.5、instanceof() 方法
instanceof() 方法可以判断一个实例对象是否是某个类的实例,或者是某个类的子类的实例。
如果一个引用变量为null
,那么对任何instanceof
的判断都为false
。
//下面Student继承自Person Person p = new Person(); System.out.println(p instanceof Person); // true System.out.println(p instanceof Student); // false Student s = new Student(); System.out.println(s instanceof Person); // true System.out.println(s instanceof Student); // true Person p = new Student(); System.out.println(p instanceof Student); // true,p实际上指向的就是student的实例 Student n = null; System.out.println(n instanceof Student); // false
可以利用instanceof
在向下转型前先判断,避免报错:
Person p = new Student(); if (p instanceof Student) { // 只有判断成功才会向下转型: Student s = (Student) p; // 一定会成功 }
3、Java中的方法重载、方法覆写和多态
3.1、方法重载(在同一个类中)
在一个类中,我们可以定义多个同名但参数列表不同的方法。方法名称相同,参数列表不同(参数的类型、个数不同)的方法就称为方法重载,与返回值无关。不过方法重载的返回值类型通常都是相同的,因为它们都是为了用来实现类似的功能。
class Hello { public void hello() { System.out.println("Hello, world!"); } public void hello(String name) { System.out.println("Hello, " + name + "!"); } }
重载是在同一个类中。
3.2、方法覆写(在子类中覆写,@Override)
在继承关系中,子类中与父类方法签名(方法名+参数列表)完全相同并且返回值类型也一样的方法,就称为方法覆写(Override)。方法覆写是方法名称、返回值类型、参数列表完全相同。
重载是在一个类中,覆写存在继承关系。
覆写的要求:
- 覆写要求被覆写的方法不能拥有比父类更加严格的访问控制权限,重载没有权限要求。
- 覆写和被覆写的方法必须同为 static 或者 非static
- 子类方法抛出的异常不能大于父类被覆写的方法的异常
//父类 class Person { public void run() { System.out.println("Person.run"); } } //子类 class Student extends Person { @Override //加上@Override可以让编译器帮助检查覆写是否正确,如果不正确编译器会报错。但Override不是必需的 public void run() { System.out.println("Student.run"); } }
在 Java 子类中,如果方法的方法名和参数列表和父类都相同,但方法返回值不同,此时编译会报错,因为此时 Java 已经会认为它们之间就是方法覆写,而返回值不同即代表覆写失败。
覆写后调用时调用的是子类的方法:
public static void main(String[] args) { Student s = new Student(); s.run(); //打印 Student.run }
3.3、多态
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型,这个特性在面向对象编程中就被称为多态。
//一个实际类型为Student,引用类型为Person的变量,调用其run()方法,实际上调用的是Student的run()方法 public static void main(String[] args) { Person p = new Student(); p.run(); // 打印Student.run }
多态的特性就是,运行期才能动态决定调用的子类方法。
//下面的方法中传入的参数类型是Person,我们无法确定传入的参数实际类型究竟是Person,还是Student,还是Person的其他子类。 //因此,无法确定调用的是Person类定义的run()方法,还是子类覆写的run()方法 public void runMethod(Person p) { p.run(); }
4、抽象类(abstract)
如果一个class
定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract
修饰。抽象方法的存在是因为有时候父类的某一方法本身不需要实现任何功能,所以并不需要执行语句,仅仅只是为了定义方法签名,目的就是为了让子类去覆写它。
如果一个类中含有抽象方法,那么就必须把该类定义为抽象类。
abstract class Person { //具有抽象方法的类必须被定义为抽象类 public abstract void run(); //方法中没有任何语句,只是为了定义然后给子类覆写,就可以写成抽象方法 public void show(); //报错 如果没有大括号{},并且没有定义为抽象,那就会报编译错误 }
把一个方法声明为abstract
,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person
类也无法被实例化,即抽象类无法被实例化。
抽象类本身被设计成只能用于被继承,因此抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错,抽象方法实际上相当于定义了规范。这个也是抽象类最大的作用,子类必须实现抽象类的抽象方法,避免了子类忘记写某一方法而出现错误的情况。
4.1、抽象类的特点
抽象类的特点:
- 抽象类无法被实例化,抽象类是用来被继承的
- 抽象类的子类必须实现抽象类中的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了 ”规范“ 。
5、接口(interface)
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样多态就能发挥出威力。
如果一个抽象类没有字段,并且所有方法全部都是抽象方法,那么我们就可以将该抽象类改写为接口 interface。接口就是抽象方法和常量的集合,它是比抽象类还要抽象的纯抽象接口,因为它连字段都没有。接口也一样不能实例化。
//抽象类 abstract class Person { public abstract void run(); public abstract String getName(); } //改写为接口 interface Person { void run(); //接口中的所有方法默认都是 public abstract,可以不写 String getName(); }
在 Java8 以后,接口中可以添加使用 default 或者 static 修饰的方法。
5.1、接口的特点
- 接口用 interface 来定义,接口是抽象方法和常量的集合
- 接口中的所有成员变量都默认是由 public static final 修饰的
- 接口中的所有方法都默认是由 public abstract 修饰的
- 接口没有构造器,无法实例化
5.2、接口继承接口(extends)
一个interface
可以继承自另一个interface
。interface
继承自interface
使用extends
,它相当于扩展了接口的方法。
interface Hello { void hello(); } //Person接口继承Hello接口后实际上将会有 2 个抽象方法签名,其中一个来自继承的Hello接口。 interface Person extends Hello { void run(); }
5.3、接口中default修饰的方法
在 Java8 以后,接口中可以添加使用 default 或者 static 修饰的方法。实现类可以不必覆写default
方法,但是会自动继承default方法,即实现类的实例对象可以直接调用default方法。
在 default 方法出现之前,当我们需要给接口新增一个方法时,会涉及到修改全部子类,因为全部实现类都需要覆写该方法。但是如果新增的是default
方法,那么子类就不必全部修改,只需要在需要覆写的实现类中进行覆写即可。
接口中的其他方法是不能有方法体的,但是用default关键字修饰的方法是可以有方法体的。
interface Person { String getName(); default void run() { System.out.println(getName() + " run"); } } class Student implements Person { private String name; public String getName() { //只需覆写抽象方法 return this.name; } }
5.4、接口中的static修饰的字段和方法
在 Java8 以后,接口中可以有静态字段和静态方法。静态字段只能是 public static final
类型,
public interface Person { public static final int NUM = 1; //可以省略不写,编译器会自动加上public statc final修饰符 int NUM2 = 1; static void testStatic() { System.out.println("static关键字"); } } //调用静态字段、静态方法 Person.NUM; Person.testStatic();
5.5、抽象类和接口的区别
抽象类和接口的对比:
抽象类是对于一类事物的高度抽象,其中既有方法又有属性,接口是对方法的抽象。
当需要对一类事物抽象的时候,应该使用抽象类,好形成一个父类用以继承。当需要对一系列的方法抽象,应该使用接口,需要实现这些方法的类去实现相应的接口。
5.6、类通过implements关键字来实现一个接口
接口的主要用途就是用于被实现类实现,一个类要实现接口,就得实现接口中的所有方法,或者是将该类定义为抽象类则无需实现接口中的方法。
class Student implements Person { private String name; public Student(String name) { this.name = name; } @Override public void run() { System.out.println(this.name + " run"); } @Override public String getName() { return this.name; } }
在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个接口。实现多个接口的类必须覆写所继承的所有接口中的全部抽象方法,或者将该类定义为抽象类也可以。
有了接口,就可以实现多重继承的效果。如果一个类既有继承,又实现接口,那么应该先继承,后实现接口
class Student implements Person, Hello { // 实现了两个interface //覆写Person和Hello中所有的抽象方法 } class Student extends Person implements Hello { //先继承,后实现接口//覆写Hello中所有的抽象方法 }
如果抽象类中新增了一个抽象方法,那么所有继承了该抽象类的类也必须得新增一个方法来覆写该新增方法,有时候这样就很不方便。此时我们可以通过接口来避免麻烦,因为类可以实现多个接口,所以一旦要新增方法,可以通过新建一个接口,需要实现的类再实现新增接口即可,其他的类不会受到影响。
6、包
一个类总是属于某个包,类名只是一个简写,真正的完整类名是包名.类名
。
在一个类中如果需要引用其他包的类的话,需要显式引入这个类才能使用,或者是直接将完整的类名即 包名.类名 写出来直接使用。
//引入Hello类 import packageName.Hello; public class Main{ public static void main(String[] args) { Hello.show(); //无需引入,直接写完整的类名 packageName.Hello.show(); } }
注意:包没有父子关系。java.util 和 java.util.zip 包只是名字不同而已,两者是不同的包,没有任何继承或者是包含关系。(java.util.zip 包的意思是包的名称就是 “java.util.zip”,但编译后class文件会放在 java/util/zip 文件夹下)
没有定义包名的class
,它使用的是默认包(default package),非常容易引起名字冲突,因此,不推荐不写包名的做法。
6.1、import 导入包
在一个类中,如果需要引用同一个包中的类的话直接使用即可,无需导入包。如果是引用其他包中的类的话,就需要使用 import 来导入完整的类名。
在写 import 的时候,可以使用 包名.*,表示把这个包下面的所有类
都导入进来。不过不推荐这种写法,因为这样就比较难看出哪个类是属于哪个包的。
// 导入Person类的完整类名 import mr.jun.Person; // 导入mr.jun包的所有class: import mr.jun.*; public class Main{ public void run() { Person p = new Person(); //同一个包下的类可以直接引用 Hello h = new Hello(); } }
Java编译器最终编译出的.class
文件只使用完整类名。在代码中,当编译器遇到一个完整类名,就会直接根据完整类名查找这个class。如果是简单类名,按下面的顺序依次查找:
- 查找当前的包是否存在这个class。实际上编译器会自动帮我们import当前的包下面的所有的类
- 查找 import 的包是否包含这个class
查找 java.lang 包是否包含这个class。实际上编译器会自动帮我们执行了默认自动 import.java.lang.*
如果按照上面的规则还无法确定类名,则编译报错。
(编译器只默认自动导入了 java.lang 包,其他的包类似 java.util 这些包仍需要我们手动导入)
6.2、包的命名
为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。
- org.apache
- org.apache.commons.log
- com.liaoxuefeng.sample
要注意不要和java.lang
包的类重名,也不要和JDK常用类重名:
- String
- System
- Runtime ...
- java.util.List
- java.text.Format
- java.math.BigInteger ...
7、jar 包
7.1、创建 jar 包
如何创建一个 jar 包,参考:https://jingyan.baidu.com/article/e6c8503cb41e7ce54f1a181e.html
7.2、调用 jar 包的方法
jar 包实际上就是包含了一个 Java 项目中 src 文件夹下的所有包,包里的所有类。在创建了 jar 包后,就可以在 Java 项目中使用这个 jar 包中的类的方法了。
如何调用 jar 包里面的方法,参考:https://jingyan.baidu.com/article/bad08e1e23982609c851219e.html
7.3、如何创建一个可执行的 jar 包并执行
创建一个 jar 包跟普通的 jar 包差别不大,不过在导出时选择的是 Runnable JAR file。导出 jar 包时会选一个类,该类中必须有 main 方法,并且要想导出为可执行 jar 包,必须得先编译一遍才行。
具体可参考:https://blog.csdn.net/shangshaohui2009/article/details/103032036
在创建了可执行 jar 包时,我们可以用命令行来执行该 jar 包:(可执行 jar 包应该双击即可执行的,但在电脑上可能会因为默认 jar 文件的打开程序设置的问题所以双击无法打开)
//在跟该 jar 包同一目录下执行下面命令 java -jar test.jar
另外我们也可以创建一个 bat 脚本文件来执行:随意创建一个 .txt 文件,将后缀改为 .bat,往该 .bat 文件中写入:
//test.jar 是你导出的 jar 包名 java -jar test.jar
然后双击执行 bat 文件即可。