JAVA面向对象基础总结
函数(field)
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}
注:同一类中不能出现方法名相同,参数相同,返回值类型不同的函数
private方法
有public
方法,自然就有private
方法。和private
字段一样,private
方法不允许外部调用
定义private
方法的理由是内部方法是可以调用private
方法的
this变量
在方法内部,可以使用一个隐含的变量this
,它始终指向当前实例。
如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this
:
class Person {
private String name;
public void setName(String name) {
this.name = name; // 前面的this不可少,少了就变成局部变量name了
}
}
可变参数
可变参数用类型...
定义,可变参数相当于数组类型:
class Group {
private String[] names;
public void setNames(String... names) {
this.names = names;
}
}
构造方法
构造方法就是在创建实例的时候就将对象中所需要的参数信息进行传入
public class Main {
public static void main(String[] args) {
Person p = new Person("Xiao Ming", 15);
System.out.println(p.getName());
System.out.println(p.getAge());
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
默认构造方法
是不是任何class
都有构造方法?是的。
那前面我们并没有为Person
类编写构造方法,为什么可以调用new Person()
?
原因是如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,类似这样:
class Person {
public Person() {
}
}
要特别注意的是,如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法
如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来
没有在构造方法中初始化字段时,引用类型的字段默认是null
,数值类型的字段用默认值,int
类型默认值是0
,布尔类型默认值是false
:
class Person {
private String name; // 默认初始化为null
private int age; // 默认初始化为0
public Person() {
}
}
多构造方法
可以定义多个构造方法,在通过new
操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分:
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this.name = name;
this.age = 12;
}
public Person() {
}
}
方法的重载
在一个类中,我们可以定义多个方法。如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。这种方法名相同,但各自的参数不同,称为方法重载(Overload
)。例如,在Hello
类中,定义多个hello()
方法:
class Hello {
public void hello() {
System.out.println("Hello, world!");
}
public void hello(String name) {
System.out.println("Hello, " + name + "!");
}
public void hello(String name, int age) {
if (age < 18) {
System.out.println("Hi, " + name + "!");
} else {
System.out.println("Hello, " + name + "!");
}
}
}
继承
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。当我们让Student
从Person
继承时,Student
就获得了Person
的所有功能,我们只需要为Student
编写新增的功能。
Java使用extends
关键字来实现继承:
class Person {
private String name;
private int age;
public String getName() {...}
public void setName(String name) {...}
public int getAge() {...}
public void setAge(int age) {...}
}
class Student extends Person {
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;
public int getScore() { … }
public void setScore(int score) { … }
可见,通过继承,Student
只需要编写额外的功能,不再需要重复代码。只要是父类拥有的方法和属性(除了private修饰的),子类都可以继承
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object
特殊,它没有父类。
注:接口的实现可以有多个
继承有个特点,就是子类无法访问父类的private
字段或者private
方法。这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private
改为protected
。用protected
修饰的字段可以被子类访问。
super
super
关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName
。
构造方法的调用
在Java中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();
阻止继承
正常情况下,只要某个class没有final
修饰符,那么任何类都可以从该class继承。
从Java 15开始,允许使用sealed
修饰class,并通过permits
明确写出能够从该class继承的子类名称。
例如,定义一个Shape
类:
public sealed class Shape permits Rect, Circle, Triangle {
...
}
上述Shape
类就是一个sealed
类,它只允许指定的3个类继承它。如果写:
public final class Rect extends Shape {...}
是没问题的,因为Rect
出现在Shape
的permits
列表中。但是,如果定义一个Ellipse
就会报错:
public final class Ellipse extends Shape {...}
// Compile error: class is not allowed to extend sealed class: Shape
原因是Ellipse
并未出现在Shape
的permits
列表中。这种sealed
类主要用于一些框架,防止继承被滥用。
sealed`类在Java 15中目前是预览状态,要启用它,必须使用参数`--enable-preview`和`--source 15
向上转型
如果一个引用变量的类型是Student
,那么它可以指向一个Student
类型的实例:
Student s = new Student();
如果一个引用类型的变量是Person
,那么它可以指向一个Person
类型的实例:
Person p = new Person();
现在问题来了:如果Student
是从Person
继承下来的,那么,一个引用类型为Person
的变量,能否指向Student
类型的实例?
Person p = new Student(); // ???
测试一下就可以发现,这种指向是允许的!
这是因为Student
继承自Person
,因此,它拥有Person
的全部功能。Person
类型的变量,如果指向Student
类型的实例,对它进行操作,是没有问题的!
这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
向上转型实际上是把一个子类型安全地变为更加抽象的父类型:
Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok
注意到继承树是Student > Person > Object
,所以,可以把Student
类型转型为Person
,或者更高层次的Object
注意:向上转型后的对象实例是无法调用子类独有的方法的,以下代码运行报错:
public class TurnClass {
public static void main(String[] args) {
Animal c = new cat("mike",9);
c.uniquemethod();
c.sonmethod();//c实例化对象无法调用cat独有的方法
}
}
class Animal{
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void bark(){
System.out.println("动物叫!");;
}
public void uniquemethod(){
System.out.println("父类独有的方法");
}
}
class cat extends Animal{
private String name;
private int age;
public cat(String name, int age) {
super(name,age);
this.name = name;
this.age = age;
}
@Override
public void bark(){
System.out.println("喵喵");
}
public void sonmethod(){
System.out.println("子类独有的方法");
}
}
向下转型
和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。例如:
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!
如果测试上面的代码,可以发现:
Person
类型p1
实际指向Student
实例,Person
类型变量p2
实际指向Person
实例。在向下转型的时候,把p1
转型为Student
会成功,因为p1
确实指向Student
实例,把p2
转型为Student
会失败,因为p2
的实际类型是Person
,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException
。
为了避免向下转型出错,Java提供了instanceof
操作符,可以先判断一个实例究竟是不是某种类型:
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
Student n = null;
System.out.println(n instanceof Student); // false
instanceof
实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null
,那么对任何instanceof
的判断都为false
。
利用instanceof
,在向下转型前可以先判断:
Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}
从Java 14开始,判断instanceof
后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
多态
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
例如,在Person
类中,我们定义了run()
方法:
class Person {
public void run() {
System.out.println("Person.run");
}
}
在子类Student
中,覆写这个run()
方法:
class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}
Override(重写/覆盖)和Overload(重载)不同的是,如果方法签名不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override
。
多态可见下图代码,Salary和StateCouncilSpecialAllowance继承了Income类,声明均采用Income声明,实例化时分别实例化为Income、Salary、StateCouncilSpecialAllowance对象,最后可根据实例化的对象不同调用不同子类的实现方法:
import java.util.Arrays;
class Main {
public static void main(String[] args) {
// 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税:
Income[] incomes = new Income[] {
new Income(3000),
new Salary(7500),
new StateCouncilSpecialAllowance(15000)
};
Main m = new Main();
System.out.println(m.totalTax(incomes));
}
public static void getnum(int... num){
System.out.println(Arrays.toString(num));
}
public double totalTax(Income... incomes) {
for (Income i:incomes) {
System.out.println(i.getTax());
System.out.println(i.toString());
}
double total = 0;
for (Income income: incomes) {
total = total + income.getTax();
}
return total;
}
}
class Income {
protected double income;
// @Override
// public String toString(){
// return "Income:name:"+income;
// }
public Income(double income) {
this.income = income;
}
public double getTax() {
return income * 0.1; // 税率10%
}
}
class Salary extends Income {
public Salary(double income) {
super(income);
}
@Override
public double getTax() {
if (income <= 5000) {
return 0;
}
return (income - 5000) * 0.2;
}
}
class StateCouncilSpecialAllowance extends Income {
public StateCouncilSpecialAllowance(double income) {
super(income);
}
@Override
public double getTax() {
return 0;
}
}
覆写Object方法
因为所有的class
最终都继承自Object
,而Object
定义了几个重要的方法:
toString()
:把instance输出为String
;equals()
:判断两个instance是否逻辑相等;hashCode()
:计算一个instance的哈希值。
在必要的情况下,我们可以覆写Object
的这几个方法。例如:
class Person {
...
// 显示更有意义的字符串:
@Override
public String toString() {
return "Person:name=" + name;
}
// 比较是否相等:
@Override
public boolean equals(Object o) {
// 当且仅当o为Person类型:
if (o instanceof Person) {
Person p = (Person) o;
// 并且name字段相同时,返回true:
return this.name.equals(p.name);
}
return false;
}
// 计算hash:
@Override
public int hashCode() {
return this.name.hashCode();
}
}
调用super
在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super
来调用。例如:
class Person {
protected String name;
public String hello() {
return "Hello, " + name;
}
}
Student extends Person {
@Override
public String hello() {
// 调用父类的hello()方法:
return super.hello() + "!";
}
}
final
继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final
。用final
修饰的方法不能被Override
:
class Person {
protected String name;
public final String hello() {
return "Hello, " + name;
}
}
Student extends Person {
// compile error: 不允许覆写
@Override
public String hello() {
}
}
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final
。用final
修饰的类不能被继承:
final class Person {
protected String name;
}
// compile error: 不允许继承自Person
Student extends Person {
}
对于一个类的实例字段,同样可以用final
修饰。用final
修饰的字段在初始化后不能被修改。例如:
class Person {
public final String name = "Unamed";
}
对final
字段重新赋值会报错:
Person p = new Person();
p.name = "New Name"; // compile error!
可以在构造方法中初始化final字段:
class Person {
public final String name;
public Person(String name) {
this.name = name;
}
}
这种方法更为常用,因为可以保证实例一旦创建,其final
字段就不可修改。
抽象类
如果一个class
定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract
修饰。
因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。
使用abstract
修饰的类就是抽象类。我们无法实例化一个抽象类。子类在继承抽象类的时候一定要实现抽象类的抽象方法,不然子类也是一个抽象类,没办法进行实例化的。
注:抽象类里面可以不包含抽象方法(一个也没有也可以),但是有抽象方法存在的类必定是抽象类
abstract class name{
public name bark(){
System.out.println("汪汪!");
return null;
}
public abstract void run();
}
接口
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。
如果一个抽象类没有字段,所有方法全部都是抽象方法:
abstract class Person {
public abstract void run();
public abstract String getName();
}
就可以把该抽象类改写为接口:interface
。
在Java中,使用interface
可以声明一个接口:
interface Person {
void run();
String getName();
}
所谓interface
,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract
的,所以这两个修饰符不需要写出来(写不写效果都一样)。
当一个具体的class
去实现一个interface
时,需要使用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中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface
,例如:
class Student implements Person, Hello { // 实现了两个interface
...
}
Java的接口特指interface
的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。
抽象类和接口的对比如下:
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class | 可以implements多个interface |
字段 | 可以定义实例字段 | 不能定义实例字段 |
抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
//抽象类里面不可以用default去定义非抽象的方法
interface Person{
int a =15;
default void run() {
System.out.println("1111");
}
}
abstract class name{
public name bark(){
System.out.println("汪汪!");
return null;
}
public abstract void run();
}
接口继承
一个interface
可以继承自另一个interface
。interface
继承自interface
使用extends
,它相当于扩展了接口的方法。例如:
interface Hello {
void hello();
}
interface Person extends Hello {
void run();
String getName();
}
此时,Person
接口继承自Hello
接口,因此,Person
接口现在实际上有3个抽象方法签名,其中一个来自继承的Hello
接口,在实现Person接口的时候需要把三个抽象方法都要实现。
静态方法
在一个class
中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。
还有一种字段,是用static
修饰的字段,称为静态字段:static field
。
实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。
对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例:
┌──────────────────┐
ming ──>│Person instance │
├──────────────────┤
│name = "Xiao Ming"│
│age = 12 │
│number ───────────┼──┐ ┌─────────────┐
└──────────────────┘ │ │Person class │
│ ├─────────────┤
├───>│number = 99 │
┌──────────────────┐ │ └─────────────┘
hong ──>│Person instance │ │
├──────────────────┤ │
│name = "Xiao Hong"│ │
│age = 15 │ │
│number ───────────┼──┘
└──────────────────┘
虽然实例可以访问静态字段,但是它们指向的其实都是Person class
的静态字段。所以,所有实例共享一个静态字段。
因此,不推荐用实例变量.静态字段
去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段
来访问静态对象。
因为静态方法属于class
而不属于实例,因此,静态方法内部,无法访问this
变量,也无法访问实例字段,它只能访问静态字段。
通过实例变量也可以调用静态方法,但这只是编译器自动帮我们把实例改写成类名而已。
接口的静态字段
因为interface
是一个纯抽象类,所以它不能定义实例字段。但是,interface
是可以有静态字段的,并且静态字段必须为final
类型:
public interface Person {
public static final int MALE = 1;
public static final int FEMALE = 2;
}
实际上,因为interface
的字段只能是public static final
类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:
public interface Person {
// 编译器会自动加上public statc final:
int MALE = 1;
int FEMALE = 2;
}
编译器会自动把该字段变为public static final
类型。
内部类
public class InnerClass {
public static void main(String[] args) {
outer o=new outer("xiaoming");
outer.Inner i=o.new Inner();
i.hello();
i.asyncHello();
}
}
class outer{
private String name;
outer(String name){
this.name=name;
}
class Inner{
void asyncHello()
{
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("hihi");
}
};
new Thread(r).start();
}
void hello(){
System.out.println("hello,"+name);
}
}
见上图代码,在outer类里再创建一个Inner类,Inner类就称为内部类
内部类的创建和调用方法,首先需要创建外部类的对象,依靠外部类的对象调研new方法创建内部类
匿名内部类
详细介绍请看:(https://blog.csdn.net/qq_34944851/article/details/51449420)
在上面代码中asyncHello方法里创建了一个匿名内部类,匿名内部类一般创建于实现和继承的状态下。
Runnable是一个接口,new Runnable(){}创建的匿名内部类实现了Runnable接口的内容,再将创建的对象名叫做r
jar包
如果有很多.class
文件,散落在各层目录中,肯定不便于管理。如果能把目录打一个包,变成一个文件,就方便多了。
jar包就是用来干这个事的,它可以把package
组织的目录层级,以及各个目录下的所有文件(包括.class
文件和其他文件)都打成一个jar文件,这样一来,无论是备份,还是发给客户,就简单多了。
jar包实际上就是一个zip格式的压缩文件,而jar包相当于目录。如果我们要执行一个jar包的class
,就可以把jar包放到classpath
中:
java -cp ./hello.jar abc.xyz.Hello
这样JVM会自动在hello.jar
文件里去搜索某个类。
那么问题来了:如何创建jar包?
因为jar包就是zip包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip
改为.jar
,一个jar包就创建成功。
假设编译输出的目录结构是这样:
package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class
这里需要特别注意的是,jar包里的第一层目录,不能是bin
,而应该是hong
、ming
、mr
。如果在Windows的资源管理器中看,应该长这样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lYa2Lt0N-1662535612229)(https://www.liaoxuefeng.com/files/attachments/1261393208671488/l)]
如果长这样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kHxyAw7N-1662535612231)(https://www.liaoxuefeng.com/files/attachments/1261391527906784/l)]
说明打包打得有问题,JVM仍然无法从jar包中查找正确的class
,原因是hong.Person
必须按hong/Person.class
存放,而不是bin/hong/Person.class
。
jar包还可以包含一个特殊的/META-INF/MANIFEST.MF
文件,MANIFEST.MF
是纯文本,可以指定Main-Class
和其它信息。JVM会自动读取这个MANIFEST.MF
文件,如果存在Main-Class
,我们就不必在命令行指定启动的类名,而是用更方便的命令:
java -jar hello.jar
jar包还可以包含其它jar包,这个时候,就需要在MANIFEST.MF
文件里配置classpath
了。
在大型项目中,不可能手动编写MANIFEST.MF
文件,再手动创建zip包。Java社区提供了大量的开源构建工具,例如Maven,可以非常方便地创建jar包。
个jar包就创建成功。
假设编译输出的目录结构是这样:
package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class
这里需要特别注意的是,jar包里的第一层目录,不能是bin
,而应该是hong
、ming
、mr
。如果在Windows的资源管理器中看,应该长这样:
[外链图片转存中…(img-lYa2Lt0N-1662535612229)]
如果长这样:
[外链图片转存中…(img-kHxyAw7N-1662535612231)]
说明打包打得有问题,JVM仍然无法从jar包中查找正确的class
,原因是hong.Person
必须按hong/Person.class
存放,而不是bin/hong/Person.class
。
jar包还可以包含一个特殊的/META-INF/MANIFEST.MF
文件,MANIFEST.MF
是纯文本,可以指定Main-Class
和其它信息。JVM会自动读取这个MANIFEST.MF
文件,如果存在Main-Class
,我们就不必在命令行指定启动的类名,而是用更方便的命令:
java -jar hello.jar
jar包还可以包含其它jar包,这个时候,就需要在MANIFEST.MF
文件里配置classpath
了。
在大型项目中,不可能手动编写MANIFEST.MF
文件,再手动创建zip包。Java社区提供了大量的开源构建工具,例如Maven,可以非常方便地创建jar包。