🚀Write In Front🚀
📝个人主页:令夏二十三
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏:Java
💬总结:希望你看完之后,能对你有所帮助,不足请指正!共同学习交流 🖊
目录
类是组成Java程序的基本要素,作为一类对象的原型,它封装了这类对象的状态和方法,实际上就是把变量和函数封装到一个类里面。
字段(field)是类的属性,其实就是变量,字段又称为域、域变量、属性和成员变量等。
方法(method)是类的功能和操作,是用函数来表示的。
下面就是一个名为Person的类,其中包含了字段和方法:
- class Person {
- String name;
- int age;
- void sayHello () {
- System.out.println ("Hello! My name is " + name);
- }
- }
通俗意义上理解,“人类”可以定义为一个类(class),而具体的人就是实例(instance)。
因此,完全可以把class看作一种数据类型,instance就是根据class创建的实例,这些实例肯定多包含类定义中的所有字段和方法,但是不同实例的具体字段可以不相同。下面就是介绍类和实例的代码示例:
- package Liao;
-
- class City {
- public String name; // 用public修饰可以使得这个字段可以被同软件包中的所有类调用
- public double latitude;
- public double longitude;
- }
-
- public class ACityTest {
- public static void main (String[] args) {
- City gz = new City (); //创建一个实例,用变量gz指向它
- gz.name = "Guangzhou";
- gz.latitude = 23.13;
- gz.longitude = 113.27;
- System.out.println (gz.name);
- System.out.println("location: " + gz.latitude + "°, " + gz.longitude + "°");
- }
- }
方法是定义在class中的操作,可以看作是一个函数,方法前面加上不同修饰标识符会有不同效果:
方法前加上 | 效果 |
public | 使得该方法可以被同文件中的所有类调用 |
private | 该方法只能被class中的其他方法调用 |
如果方法的形参名和类的字段名相同,则在方法内使用字段的时候,必须使用this来区分开;
在调用方法时,若有参数,必须严格填写参数,此外,还可以设置非法输入的提示语句。
下面是一个计算年龄的示例:
- package Liao;
-
- import java.util.*;
-
- class Body {
- private String name;
- private int birth;
-
- public void setName(String name) {
- if (name == null || name.isEmpty()) {
- throw new IllegalArgumentException("invalid input!"); // 非法提示 }
- this.name = name;
- }
-
- public void setBirth(int birth) {
- if (birth < 0 || birth > 2023) {
- throw new IllegalArgumentException("invalid input!");
- }
- this.birth = birth;
- }
-
- public String getName() {
- return name;
- }
-
- public int getBirth() {
- return this.birth;
- }
-
- public int getAge() {
- return calcAge(2023);
- }
-
- private int calcAge(int currentYear) {
- return currentYear - this.birth;
- }
-
- public void replyBody(String name, int birth) {
- System.out.printf("Hi %s, I'm very glad to tell you that you are really %d years old!", name, getAge());
- }
- }
-
- public class BMethodTest2 {
- public static void main(String[] args) {
- Body B = new Body();
- Scanner scanner = new Scanner(System.in);
- System.out.print("Please input your name: ");
- String name = scanner.next();
- System.out.println();
- B.setName(name);
- System.out.print("Please enter your birth year: ");
- int birth = scanner.nextInt();
- B.setBirth(birth);
- System.out.println();
- System.out.printf("Hello %s, you are %d years old.", B.getName(), B.getAge());
- System.out.println();
- B.replyBody(B.getName(), B.getBirth());
- }
- }
当方法的参数为数组时,参数值可以传递,对于数组参数,其格式较为特殊,见下例:
- package Liao;
-
- import java.util.*;
-
- class Group {
- private String[] names;
- private int[] ages;
-
- public void setAges(int[] ages) {
- this.ages = ages;
- }
-
- public void setNames(String... names) {
- this.names = names;
- }
-
- public String getName(int i) {
- return this.names[i-1];
- }
- }
-
- public class BMethodTest3 {
- public static void main(String[] args) {
- Group g = new Group();
- g.setNames("Tom", "Jane", "Andy");
- System.out.printf("Please enter the index: ");
- Scanner scanner = new Scanner(System.in);
- int i = scanner.nextInt();
- System.out.print(g.getName(i));
- g.setAges(new int[] {17, 18, 19});
- }
- }
从上面的代码中可以知道,方法的参数为数组时,直接写一般格式需要在调用时新建一个数组,如果用 “...” 来代替方括号,则可以直接往调用的括号里填数据元素值,无论填多少都可以,这时候的数组是动态的。
一般来说,我们使用一个类来创建实例后,接下来要给实例赋值,如果实例的field是public的,那么可以直接赋值,如果是private的,就得使用类里面定义的set方法来赋值了。
这样赋值的话,效率很低,因为对每个field赋值都得写一条语句,那么有没有方法一句就把值都赋了呢?当然有了,这就需要构造方法派上用场了。
构造方法其实就是在类里面定义的赋值方法,它和所在的类同名,而且没有返回值,这两个特点就足以知道它有多重要了。在构造方法的参数列表中,可以写上与我们想要赋值的field的数据类型相同的形参,在方法的函数体里就可以用this对它们逐一赋值了;
那怎么使用构造方法呢?其实和我们使用类创建实例一模一样,只不过在使用构造方法创建实例时,括号里面就得写上想要给字段赋的值了。这不仅让我们浮想联翩,为什么会一样呢?实际上,当我们定义一个类但没有写构造方法时,系统就默认产生一个构造方法,只不过这个方法没有参数,所以我们一开始用类创建对象时,用的就是默认的构造方法!
如果我们定义了一个带参数的构造方法后,那这个方法就变成默认的了,这时候想要使用原先的无参数构造方法,就需要我们在类定义里面再写一个空的构造方法;
同时,构造方法也是可以有很多种的,它们名字都必须和类同名,但是参数列表可以不一样,当我们使用不同参数的构造方法时,系统会自动匹配,特别智能!而且不同的构造方法间也可以相互调用,只不过使用时要用this来代替名字,也要使用正确的调用格式。
最后强调一点,在类里面定义字段时,其实是可以赋初值的,但是使用带参数的构造方法之后,真正的值就是构造方法赋予的了,此时如果使用不带参数的默认构造方法,那么实例的各个字段的值就是我们定义字段时赋的值了,另外,就算我们没有给字段赋值,字段也会有默认的初值,比如String型的初值为null,int型的初值为0。
说了这么多,其实就是我对字段的一些小理解,说服力不一定够,但是看了下面的代码,一定就可以理解透了,复习的时候跟着敲一敲试一下效果是很不错的!下面是构造方法的代码示例:
- package Liao;
-
- class Mankind {
- private String name = "Andy";
- private int age = 18;
-
- public Mankind() {
- }
-
- public Mankind(String name, int age) {
- this.name = name;
- this.age = age;
- }
-
- public Mankind(String name) {
- this.name = name;
- }
-
- public Mankind(int age) {
- this("Altman", age);
- }
-
- public String getName() {
- return this.name;
- }
-
- public int getAge() {
- return this.age;
- }
- }
-
- public class CStructTest1 {
- public static void main(String[] args) {
- Mankind man = new Mankind("Any", 24);
- Mankind woman = new Mankind();
- Mankind child = new Mankind("Ace");
- Mankind elder = new Mankind(99);
- System.out.printf("%s is %d years old now.", man.getName(), man.getAge());
- System.out.println();
- System.out.printf("%s is %d years old now.", woman.getName(), woman.getAge());
- System.out.println();
- System.out.printf("%s is %d years old now.", child.getName(), child.getAge());
- System.out.println();
- System.out.printf("%s is %d years old now.", elder.getName(), elder.getAge());
- }
- }
意思就是,如果有一些方法的功能很接近,比如都是输出或查找,那就可以将它们的名字设置为同一个,只要把参数的类型或数量改一改就好,这样就叫做方法重载,示例如下:
- package Liao;
-
- 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 + "!");
- }
- }
-
- public class DOverloadTest1 {
- public static void main(String[] args) {
- Hello h = new Hello();
- h.hello("Tom", 19);
- }
- }
继承可以复用代码,当我们让 Students 从 Persons 继承时,Students 就获得了 Persons 的所有字段和方法,我们只需要为 Students 编写新增的字段和方法即可。
在Java中,使用关键字 extends 来实现继承,请看下面的示例:
- package Liao;
-
- class Persons {
- protected String name;
- private int age;
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public int getAge() {
- return age;
- }
-
- public void setAge(int age) {
- this.age = age;
- }
-
- public Persons(String name, int age) {
- this.name = name;
- this.age = age;
- }
- }
-
- class Students extends Persons {
-
- private int score;
-
- public int getScore() {
- return score;
- }
-
- public void setScore(int score) {
- this.score = score;
- }
-
- public Students(String name, int age, int score) {
- super(name, age);
- this.setName(name);
- this.setAge(age);
- this.setScore(score);
- }
-
- public String hello() {
- return "Hello, " + name + ".";
- }
- }
-
- public class EInheritTest1 {
- public static void main(String[] args) {
- Students s = new Students("Tom", 18, 100);
- System.out.println(s.getName() + " is " + s.getAge() + " years old, and he gets a " + s.getScore() + " score, congratulate to him!");
- System.out.println(s.hello());
- }
- }
但是需要注意的是,子类既然自动获得了父类的所有字段,那就严禁再定义与父类重名的字段!
在面向对象程序设计中,我们把上面示例中的 Persons 称为父类、超类、基类,把 Students 称为子类、扩展类。
Java只允许class继承自一个类,也就是说儿子只能有一个父亲,但是父亲可以有多个孩子,没有指定父类的class就默认继承自 Object。
基于上面的示例,我们再定义一个继承自 Person 的类 Teacher,则这些类的继承树如下:
在继承中,子类无法访问父类中由private修饰的字段和方法,这使得继承有了局限性。
为了让子类可以访问父类的字段,我们需要把父类的private改成protected,用protected修饰的字段可以被子类访问,具体见上面的示例。
因此,protected关键字可以把字段和方法的访问权限控制在继承树内部。
super关键字表示父类,子类引用父类的字段时,可以用super来代指父类,例如:
- class Student extends Person {
- public String hello() {
- return "Hello " + super.name;
- }
- }
在这个例子中,使用 super.name 的效果和 this.name 以及 name 是一样的。
需要注意的是,构造方法是无法默认就继承的。所以,在子类中定义构造方法时,如果父类没有默认的构造方法(也就是再定义了其他的构造方法),就需要在子类构造方法的开端就明确调用super()并在括号中写明参数以便让编译器定位到父类的一个适配的构造方法。示例如下:
- class Student extends Person {
- protected int score;
- public Student(String name, int age, int score) {
- super(name, age);
- this.score = score;
- }
- }
一般情况下,只要某个class没有final修饰符,那么任何类都可以从该类继承。
还有一种特殊的修饰符,名叫sealed,用sealed来修饰一个类,再在后面用permits明确可以从该类继承的子类名称,这样可以防止某个类被胡乱继承。示例如下:
- public sealed class Shape permits Rect, Circle, Triangle {
-
- }
顺着继承树向上转变类型就叫做向上转型,其实就是将子类类型变为父类类型:
- Student s = new Student();
- Person p = s;
- Object o1 = p;
- Object o2 = s;
-
- Person p = new Student(); // 这里以前面的为准
把一个父类类型强制转化为子类类型,就是向下转型,在转型前一般要用到一个判断符 instanceof,放在 if 语句里判断前者是否为后者的父类,如果是,则直接把前者转换为后者,也就是把父类转换为子类,不需要再写强制转换:
- Person p = new Student();
- if (p instanceof Student) {
- ...
- }
在使用继承时,要注意逻辑一致性,比如我们定义了Person,Student 和 Book,那么我们在逻辑上可以让 Student 继承 Person,虽然让 Student 继承 Book 也可以实现,但不符合逻辑,因为Book 只能是从属于 Student ,而不能成为其父类,所以如果要使 Student 能够拥有 Book 的字段和方法,就可以在 Student 里定义一个Book,这样就是把 Student 和 Book组合起来了,此时Student 持有一个 Book 实例:
- class Book {
- protected String name;
- public String getName() {
- return this.name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
- class Person {
- protected int height;
- protected int weight;
- }
- class Student extends Person {
- int score;
- protected Book book;
- }
在子类中定义一个和父类方法的返回值和名字相同的方法,称为重写 Override。例如:
- class Person {
- protected int height;
- protected int weight;
- public void run() {
- System.out.println("Person.run");
- }
- }
- class Student extends Person {
- int score;
- protected Book book;
- public void run() {
- System.out.println("Student.run");
- }
- }
而方法重载指的是在一个类里,方法名字相同,但是参数不同,返回类型可以相同也可以不同。
利用多态,我们可以在子类中重写父类的方法,在调用子类时,系统会自动判断子类的类型,并调用对应的重写过的方法。
再重写一个方法之前,要记得加上这句:@Override
下面是一个计算税费的例子,我们给一个有三种收入的大佬算税:
- package Week_9;
-
- public class Tax_practice {
- public static void main(String[] args) {
- Income[] incomes = new Income[] {
- new Income(3000),
- new Salary(7500),
- new StateCouncilSpecialAllowance(15000)
- };
- System.out.println(totalTax(incomes));
- }
- public static double totalTax(Income... incomes) {
- double total = 0;
- for (Income income:incomes) {
- total += income.getTax();
- }
- return total;
- }
- }
- class Income {
- protected double income;
- public Income(double income) {
- this.income = income;
- }
- public double getTax() {
- return income * 0.1;
- }
- }
- 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;
- }
- }
所有的class都继承自Object,而Object定义了几个重要的方法:
在具体的class中,我们可以结合class的自身特征来重写这几个Object方法。
在子类的重写方法中,要调用父类的被重写方法,就在方法前加上 super 即可。
如果一个父类不允许子类对他的某一个方法进行重写,可以把该方法标记为 final, 用 final修饰的方法不能被重写。
如果一个 class 定义了方法,没有具体内容,这个方法就是抽象方法,需要用 abstract 修饰;
只有抽象类才能执行抽象方法,所以在这个 class 前也要加上 abstract 来修饰。
抽象类无法直接实例化,只能被继承,而且继承抽象类的子类必须重写抽象方法,否则会报错,示例如下:
- public class Abs_practice {
- static abstract class Person {
- public abstract void run();
- }
- static class Student extends Person {
- @Override
- public void run() {
- System.out.print("student.run");
- }
- }
- public static void main(String[] args) {
- Student s = new Student();
- s.run();
- }
- }
如果我们定义了抽象类以及具体子类,我们就可以用抽象类去引用具体子类的实例,这样不需要子类就可以实现对子类的字段方法的使用:
- Person s = new Student();
- Person t = new Teacher();
-
- s.run();
- t.run();
这就是面向抽象编程,我们只关心抽象方法的定义,不关心子类的具体实现。
接口相当于没有字段的抽象类,里面只含有抽象方法,这里我们可以简化掉所有 abstract ,使用关键字 interface 来声明一个接口:
- interface Person {
- void run();
- String getName();
- }
当一个具体的 class 想要使用接口时,和从抽象类继承相似,不过要把 extends 换成 implements:
- public class Interface_practice {
- interface Person {
- void run();
- String getName();
- }
- 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 可以继承自另一个 interface, 这里的继承要用 extends,相当于扩展了接口的方法,例如:
- interface Person {
- void run();
- String getName();
- }
- interface People extends Person {
- void hello();
- }
同样,接口也可以向上转型,就是定义一个父类接口,将子类接口实例赋值给它即可。
用 default 修饰 interface 的方法,这样有 class 调用这个接口时就可以不用重写这个方法了,但是要注意,default 方法无法访问字段,而抽象类的普通方法可以访问实例字段。
普通的实例字段是独立的,每个实例的同名字段都有自己的空间,但是如果在class中定义了一个用 static 修饰的字段,这就叫做静态字段,静态字段是被这个class的所有实例共享的;
一般不通过实例来访问静态字段,而是直接用 类名.静态字段 这样的格式来访问,可以把静态字段理解为描述class本身的字段。
用 static 修饰 class 里的的方法,就可以得到静态方法,静态方法一般也是直接用类名来使用,静态方法属于 class 而不属于实例,所以静态方法内部无法访问 this 变量,也无法访问实例字段,只能访问静态字段。
在 interface中其实是可以定义字段的,只不过这里的字段默认是静态字段,而且还是用final修饰的。