JAVA学习之面向对象
PART ONE:面向对象基础
1.类与对象:
类是一种抽象的概念,它描述了一类具有相似属性和行为的对象的集合。类定义了对象的属性和行为,并且可以作为创建对象的模板。
对象是类的实例,它是类的具体实现。对象具有类定义的属性和行为,并且可以在程序中被创建、操作和销毁。对象是程序中的实体,它可以被用来执行特定的任务和操作。对象是类的具体实例化,每个对象都有自己的状态和行为
2.面向对象的流程:
写一个简单的代码实现面向对象的三个流程:
public class Dog {
String name;
int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public void bark() {
System.out.println(name + " is barking.");
}
public void fetch() {
System.out.println(name + " is fetching.");
}
public void sleep() {
System.out.println(name + " is sleeping.");
}
}
2.创建一个对象并调用方法
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog("Buddy", 3);
System.out.println("My dog's name is " + myDog.name + " and it is " + myDog.age + " years old.");
myDog.bark();
myDog.fetch();
myDog.sleep();
}
}
这样就实现了面向对象的三个流程:创建类、类的实例化、调用类的属性或者方法。
3.类的储存结构:
4.成员变量与局部变量:
Java类的成员变量是指定义在类中的变量,它们可以被整个类的方法访问和使用。成员变量可以是任何数据类型,包括基本数据类型和引用数据类型。成员变量通常用来表示对象的属性或状态。因此,成员变量又叫做属性。
局部变量是定义在方法、构造函数或代码块中的变量,它们只在定义它们的作用域内可见。局部变量在方法执行时创建,并在方法执行结束时销毁。局部变量不能被其他方法访问,也不能被其他对象访问。局部变量通常用来存储临时数据或方法的参数。
成员变量和局部变量的区别在于作用域和生命周期。成员变量的作用域是整个类,它们的生命周期与对象的生命周期相同;而局部变量的作用域是定义它们的方法、构造函数或代码块,它们的生命周期只在方法执行期间有效。
public class Example {
// 成员变量
private int memberVariable;
public void exampleMethod() {
// 局部变量
int localVariable = 10;
System.out.println(localVariable);
}
}
注意,属性可以用权限修饰符,而局部变量不行。
5.方法:
在Java中,方法是类中定义的用于执行特定任务的代码块。方法也可以被称为函数。它们用于封装一组操作,使代码更加模块化和可重用。方法通常用于执行特定的操作,计算一些值或者改变对象的状态。
在Java中,方法由方法名、参数列表、返回类型、方法体组成。方法名用于标识方法,在调用方法时使用。参数列表指定了方法接受的输入参数,返回类型指定了方法返回的数据类型,方法体包含了方法的实际执行代码。
方法可以被其他代码调用,通过调用方法可以执行其中定义的操作,并且可以返回一个值。在面向对象编程中,方法通常用于表示对象的行为或功能,例如一个汽车对象可能有启动、加速、刹车等方法。方法的使用可以使代码更加清晰、模块化,并且可以提高代码的可重用性。
当我们创建一个类来表示汽车时,我们可以定义一些方法来表示汽车的行为,比如:
public class Car {
// 成员变量
private String brand;
private int speed;
// 构造方法
public Car(String brand) {
this.brand = brand;
this.speed = 0;
}
// 加速方法
public void accelerate(int increment) {
speed += increment;
}
// 刹车方法
public void brake(int decrement) {
speed -= decrement;
}
// 获取速度方法
public int getSpeed() {
return speed;
}
}
在这个例子中,我们定义了一个Car
类,其中包括了brand
和speed
两个成员变量,以及accelerate
、brake
和getSpeed
三个方法。accelerate
方法用于加速汽车的速度,brake
方法用于减速汽车的速度,getSpeed
方法用于获取汽车的当前速度。这些方法代表了汽车对象的行为或功能。当我们创建Car
对象时,我们可以调用这些方法来操作汽车的速度。
方法声明的格式:
修饰符:Java方法的修饰符是用来限定方法的访问权限和行为的关键字。常见的修饰符包括public、private、protected和static等。public表示方法可以被任何类访问,private表示方法只能被当前类访问,protected表示方法可以被当前类和其子类访问,static表示方法属于类而不是对象。
返回值:Java方法的返回值是指方法执行完毕后返回的数据类型。返回值可以是任何合法的数据类型,包括基本数据类型和引用数据类型。如果方法不需要返回值,则返回类型应该使用void关键字。
方法名:Java方法的方法名是指方法的名称,用来唯一标识方法。方法名可以是任何合法的标识符,但应该符合命名规范,通常使用动词或动词短语来描述方法的功能。
形参列表:Java方法的形参列表是指方法定义时包含的参数列表。形参列表由参数的数据类型和参数名组成,多个参数之间用逗号分隔。形参列表可以为空,也可以包含任意数量的参数。参数的数据类型可以是任何合法的数据类型,包括基本数据类型和引用数据类型。
注意,与其他语言的函数不一样的是,java
中可以像C++
类中调用函数来改变类中属性的值:
class student{
int age;
void add_age(){
age+=2;
}
}
public class test {
public static void main(String[] args){
student s1 = new student();
s1.age=12;
s1.add_age();
System.out.print(s1.age);
}
}
6.方法的重载与可变个数形参:
方法的重载是指在同一个类中,可以定义多个同名方法,但它们的参数列表或参数类型不同,从而实现不同的功能。在调用这个方法时,编译器会根据传入的参数类型或数量来确定调用哪个重载方法。方法的重载可以提高代码的灵活性和可读性,使得同名方法可以根据不同的参数类型或数量来执行不同的操作。
当我们定义一个名为add的方法时,可以根据不同的参数类型或数量来进行重载,例如:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
在上面的例子中,我们定义了三个名为add的方法,它们的参数列表分别为(int, int)、(double, double)和(int, int, int),这样在调用add方法时,编译器会根据传入的参数类型或数量来确定调用哪个重载方法。
在Java中,我们可以使用可变参数来定义一个方法,这样可以接受不定数量的参数。可变参数在方法内部被当作数组来处理。
下面是一个使用可变参数的方法的例子:
public class VariableArgumentsExample {
public void printNumbers(int... numbers) {
for (int number : numbers) {
System.out.print(number + " ");
}
System.out.println();
}
public static void main(String[] args) {
VariableArgumentsExample example = new VariableArgumentsExample();
example.printNumbers(1, 2, 3); // 可以传入任意数量的参数
example.printNumbers(4, 5, 6, 7, 8, 9, 10);
}
}
在上面的例子中,printNumbers方法使用了可变参数int… numbers来接收不定数量的整数参数。在方法内部,numbers被当作一个整数数组来处理,我们可以通过for循环遍历这个数组,对传入的参数进行处理。
注意,可变形参本质是是数组,因此与方法名相同的且为数组的方法会冲突。其次,可变形参必须是形参列表的最后。
7.值的传递机制:
在java
中,如果是基本数据类型,则将变量保存的值传递出去,如果是引用数据类型,则传递地址。
//未传参
int a = 3;
int b = a;
b = 4;
System.out.print(a);
//传参
int a = 3;
add(a);//
System.out.print(a);
public void add(int m){
m++;
}
a的值还是3,传得是值。
class student{
int age;
}
public class test {
public static void main(String[] args){
student a = new student();
a.age = 1;
test.am(a);
System.out.print(a.age);
}
public static void am(student p){
p.age++;
}
}
a的值变成了2,因为传的是地址。
注意,如果这么写,它事实上传递的还是基础数据类型,a还是1:
class student{
int age;
}
public class test {
public static void main(String[] args){
student a = new student();
a.age = 1;
test.am(a.age);
System.out.print(a.age);
}
public static void am(int p){
p++;
}
}
8.关于static的规则:
在Java中,非静态方法(实例方法)需要一个对象的实例来调用。如果方法是一个非静态方法,是不能在 main
方法中调用的。而main
方法是静态的,这意味着它可以在没有对象实例的情况下被调用。因此,如果想在 main
方法中调用一个非静态方法,需要首先创建该类的一个对象,然后使用该对象来调用方法。
然而,静态方法属于类本身,而不是类的任何对象。因此,它们可以在没有创建类的对象的情况下被调用。这就是为什么在有些情况下, 方法需要被声明为静态的,这样它就可以在 main
方法中直接被调用了。
这是一个简单的例子来说明这个概念:
public class Test {
public static void main(String[] args) {
// 调用静态方法,无需创建对象
staticMethod();
// 创建对象
Test test = new Test();
// 使用对象调用非静态方法
test.nonStaticMethod();
}
public static void staticMethod() {
System.out.println("This is a static method.");
}
public void nonStaticMethod() {
System.out.println("This is a non-static method.");
}
}
在这个例子中,staticMethod
是静态的,所以它可以在 main
方法中直接被调用。nonStaticMethod
是非静态的,所以我们需要创建一个 Test
类的对象,然后使用这个对象来调用它。
9.package与import:
在Java中,package
和 import
是用来组织和访问类的重要工具。
Package(包):
- 包是用来组织相关类和接口的一种机制。您可以把包想象成文件系统中的文件夹,其中包含了相关的类文件。
- 包的主要目的是为了防止命名冲突,更好地组织代码,控制访问权限以及提供搜索和定位类等功能。
- 每个Java源文件的顶部都可以声明一个包。例如,
package com.example.myapp;
就声明了一个名为com.example.myapp
的包。 - 如果一个类没有声明包,那么它属于默认包(无名包)。但是,通常建议明确声明包,以便更好地组织代码。
- 不要使用Java作为包名字。
Import(导入):
import
关键字用于在当前源文件中导入其他Java包中的类或接口。- 通过使用
import
,我们可以使用其他包中的类,而无需每次都写出完整的类名。 - 例如,
import java.util.List;
允许我们在代码中直接使用List
,而不是java.util.List
。 - 我们也可以使用
*
来导入一个包中的所有类,例如import java.util.*;
。 - 如果导入的类或者接口是
java.lang
包下的,或者是当前包下的,那么可以省略此步骤。 - 如果已经导入
java.lang
包下的类,那么如果需要使用java.lang
包下的子包下的类,那么还是需要import
。 - 在Java中,
import static
是一种特殊的导入语句,它允许我们直接访问其他类中的静态成员,而无需每次都写出类名。
这是一个简单的例子来说明 package
和 import
的使用:
// 声明包
package com.example.myapp;
// 导入ArrayList类
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
// 由于我们导入了ArrayList,所以可以直接使用
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
System.out.println(list.get(0));
}
}
在这个例子中,我们声明了一个名为 com.example.myapp
的包,并导入了 java.util.ArrayList
类。这使得我们可以在 main
方法中直接使用 ArrayList
。
10.封装性:
在Java中,封装是面向对象编程的四大基本特性之一,其他三个是继承、多态和抽象。
封装(Encapsulation)的主要目的是增强安全性并简化编程。封装可以隐藏对象的内部状态,并防止用户直接访问它们。相反,用户通过对象提供的公共方法(通常称为 getter 和 setter)来访问和修改这些状态。
以下是一个简单的封装示例:
public class Student {
// 将 age 属性设为私有,这样它就不能被外部类直接访问
private int age;
// 提供一个公共的 getter 方法来获取 age 的值
public int getAge() {
return age;
}
// 提供一个公共的 setter 方法来设置 age 的值
// 这里可以添加一些验证逻辑,例如检查年龄是否为负数
public void setAge(int age) {
if (age >= 0) {
this.age = age;
} else {
System.out.println("Age cannot be negative.");
}
}
}
在这个例子中,age
属性被封装在 Student
类中。外部类不能直接访问 age
,而必须通过 getAge
和 setAge
方法来获取和设置 age
的值。这样,Student
类就可以控制谁可以访问 age
,以及何时可以访问。此外,Student
类还可以在 setAge
方法中添加验证逻辑,以确保 age
不会被设置为无效值。
封装的好处包括:
-
增强安全性:隐藏对象的内部状态可以防止用户在不应该的时候修改它,或者在不理解其含义的情况下修改它。
-
提高可维护性:封装可以将代码的实现细节隐藏起来,使得用户只需要关心接口,而不需要关心实现。这样,即使实现发生了变化,只要接口保持不变,用户的代码就不需要修改。
-
简化编程:封装可以隐藏复杂的实现细节,使得用户可以在更高的抽象层次上进行编程。
修饰符:
在Java中,有四种访问权限修饰符,它们定义了类、变量、方法和构造器的可见性。这四种修饰符分别是:public
、protected
、private
和 默认(无修饰符)。
- public:如果一个类、方法或变量被声明为
public
,那么它可以在任何地方被访问。这是最宽松的访问级别。 - protected:如果一个类、方法或变量被声明为
protected
,那么它只能在以下地方被访问:- 同一个包中的其他类
- 该类的所有子类
- private:如果一个类、方法或变量被声明为
private
,那么它只能在声明它的类中被访问。这是最严格的访问级别。 - 默认(无修饰符):如果一个类、方法或变量没有使用访问修饰符,那么它的访问级别是“包私有的”(package-private),也就是说,它只能在同一个包中的其他类被访问。
这是一个简单的例子来说明这四种访问修饰符:
package com.example;
public class Test {
public int publicVar;
protected int protectedVar;
private int privateVar;
int defaultVar; // 没有使用访问修饰符
public void publicMethod() {}
protected void protectedMethod() {}
private void privateMethod() {}
void defaultMethod() {} // 没有使用访问修饰符
}
在这个例子中,publicVar
和 publicMethod
可以在任何地方被访问,protectedVar
和 protectedMethod
只能在同一个包中的其他类或 Test
的子类中被访问,privateVar
和 privateMethod
只能在 Test
类中被访问,defaultVar
和 defaultMethod
只能在同一个包中的其他类被访问。
封装性的举例:
// 定义一个类,其中有一个私有变量
public class MyClass {
private int myValue;
// 提供一个公共方法来修改私有变量的值
public void setMyValue(int value) {
myValue = value;
}
// 提供一个公共方法来获取私有变量的值
public int getMyValue() {
return myValue;
}
}
// 定义另一个类,它使用公共方法来修改和打印私有变量的值
public class Test {
public static void main(String[] args) {
MyClass obj = new MyClass();
// 使用公共方法来修改私有变量的值
obj.setMyValue(10);
// 使用公共方法来获取并打印私有变量的值
System.out.println(obj.getMyValue());
}
}
在这个例子中,MyClass
类有一个私有变量 myValue
,以及两个公共方法 setMyValue
和 getMyValue
。Test
类创建了一个 MyClass
的对象,并使用这些公共方法来修改和打印 myValue
的值。这就是封装的一个例子:myValue
的内部状态被隐藏起来,只能通过 MyClass
提供的公共方法来访问和修改。
11.构造器与初始化:
在Java中,构造器(Constructor)是一种特殊的方法,类似于C++的构造函数,用于初始化新创建的对象。构造器的名称必须与类名相同,并且它没有返回类型。以下是构造器的一些关键特性:
- 创建对象:当使用
new
关键字创建类的新对象时,会自动调用构造器来初始化该对象。 - 与类名相同:构造器的名称必须与类名完全相同。
- 无返回类型:构造器没有返回类型,甚至连
void
都没有。 - 重载:一个类可以有多个构造器,它们的参数列表各不相同。这被称为构造器的重载。
以下是一个简单的例子,其中定义了一个类和它的两个构造器:
public class Student {
private String name;
private int age;
// 无参数构造器
public Student() {
this.name = "";
this.age = 0;
}
// 带参数构造器
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
在这个例子中,Student
类有两个构造器:一个是无参数构造器,另一个是带参数构造器。当创建 Student
对象时,可以选择调用哪一个构造器,例如:
Student s1 = new Student(); // 调用无参数构造器
Student s2 = new Student("Alice", 20); // 调用带参数构造器
创建类时,如果未定义构造器,则默认存在一个无参数构造器。且构造器的权限与类相同。一旦声明了构造器,则系统不在默认提供构造器。
匿名对象:
匿名对象是没有名称的对象,它们只能在创建时使用。匿名对象主要用于执行一次性操作。
class MyClass {
public:
void doSomething() {
// do something
}
};
int main() {
// 创建匿名对象并调用成员函数
MyClass().doSomething();
// 将匿名对象作为函数实参
someFunction(MClass());
return 0;
}
它还可以作为实参传递给方法。
类的属性初始化及先后顺序:
12.Javabean
:
JavaBean
是一种符合特定约定的Java
类,用于在Java
应用程序中封装数据和行为。一个JavaBean
类通常包含:
-
私有的成员变量(属性),
-
公共的
getter
和setter
方法, -
一个无参的构造方法。
这些属性和方法用于对类的状态进行操作和访问。
JavaBean
通常用于在Java
应用程序中实现数据封装和业务逻辑处理。它们可以被其他Java
类访问和操作,从而实现了类之间的松耦合和模块化设计。JavaBean
还可以通过反射机制进行动态访问和操作,使得它们在Java
的框架和工具中得到广泛应用。
当创建一个简单的JavaBean
类时,可以按照以下方式定义:
public class Person {
private String name;
private int age;
public Person() {
// 无参构造方法
}
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;
}
}
在这个例子中,Person
类包含了私有的name
和age
属性,以及公共的getter和setter方法。这些方法用于获取和设置Person
对象的属性值。这样,其他类就可以通过调用这些方法来访问和修改Person
对象的属性。
使用这个Person
类的示例代码如下:
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.setName("Alice");
person.setAge(25);
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
}
}
在这个示例中,我们创建了一个Person
对象,并通过调用其setter方法设置了姓名和年龄,然后通过调用getter方法获取了这些属性的值并进行打印输出。
13.UML
类图:
UML
(Unified Modeling Language)类图是一种用于描述类结构和类之间关系的图形化建模工具。它是一种标准化的图形语言,用于可视化和表达软件系统的设计和结构。在UML
类图中,类被表示为矩形框,其中包含类的名称、属性和方法。类之间的关系通常通过箭头表示,包括继承、关联、聚合、组合等。UML
类图是软件工程师和开发团队之间交流和沟通的重要工具,可以帮助他们理解和设计复杂的软件系统。
PART TWO:面向对象进阶
1.this
关键字:
在Java中,this是一个关键字,它用于引用当前对象的实例。this关键字可以在类的方法中使用,以便访问当前对象的属性和方法。
在Java中,this关键字通常用于解决实例变量和局部变量之间的命名冲突。当实例变量和局部变量具有相同的名称时,可以使用this关键字来明确地指示使用实例变量。例如:
public class Person {
private String name;
public void setName(String name) {
this.name = name; // 使用this关键字来引用当前对象的name属性
}
}
在上面的例子中,this.name
指的是当前对象的name属性,而name则是方法的参数。
此外,this关键字还可以在一个构造方法中调用另一个构造方法。这种用法称为构造方法的重载。例如:
public class Person {
private String name;
private int age;
public Person() {
this("John Doe", 30); // 调用另一个构造方法
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
在这个例子中,第一个构造方法调用了第二个构造方法,并使用this关键字传递了参数。
总的来说,this关键字在Java中用于引用当前对象的实例,以便访问其属性和方法,或者在构造方法中调用另一个构造方法。
注意,在调用另外一个构造器时,只用写this(),或者在()内部写上参数。而且有以下规则:
2.快捷操作:
在idea
中,我们可以用insert+alt
进行快捷操作:
3.继承性:
3.1.继承性入门:
Java的继承性是指一个类可以从另一个类中继承属性和方法。继承性是面向对象编程中的重要概念,它允许一个类(子类)可以使用另一个类(父类)的属性和方法,从而可以减少代码重复,提高代码的可重用性和可维护性。
Java的继承性具有以下特点:
-
单继承:Java中的类只能继承一个直接父类,即一个子类只能有一个直接父类。
-
多层继承:一个类可以有多个子类,子类可以再派生出新的子类,形成多层继承关系。
-
继承链:通过继承,可以形成一个类的继承链,即一个子类可以继承其直接父类的属性和方法,同时也可以继承其父类的父类的属性和方法,以此类推。
-
子类可以重写父类的方法:子类可以根据自己的需求重写父类的方法,从而实现多态性。
-
继承关系不会改变类的访问权限:子类继承了父类的属性和方法,但并不改变其访问权限,即子类无法访问父类的私有成员。
继承性是Java面向对象编程中的重要特性,它能够提高代码的重用性和可维护性,同时也能够实现多态性,使得程序更加灵活和可扩展。
在Java中,继承的格式是使用关键字“extends”来表示一个类继承另一个类。例如:
public class SubClass extends SuperClass {
// 子类的成员变量和方法
}
在这个例子中,SubClass
是子类,SuperClass
是父类。子类继承了父类的属性和方法。通过继承,子类可以重用父类的代码,并且可以添加新的属性和方法。
在Java中,子类是无法继承父类的私有成员变量和私有方法的。私有成员变量和私有方法只能在定义它们的类中访问,无法被其他类继承或访问。如果子类需要访问父类的私有成员变量或私有方法,可以通过父类提供的公共方法来间接访问。
3.2.默认的父类:
在java
中,如果没有显式声明其父类,则默认继承于Java.lang.object
3.3.类名的获取:
在Java中,你可以使用getClass()
方法来获取一个对象的类,然后使用getSuperclass()
方法来获取它的父类。下面是一个示例:
public class SuperClass {
// 父类的内容
}
public class SubClass extends SuperClass {
public static void main(String[] args) {
SubClass sub = new SubClass();
Class<?> subClass = sub.getClass();
Class<?> superClass = subClass.getSuperclass();
System.out.println("SubClass的父类是: " + superClass.getName());
}
}
在这个示例中,我们创建了一个SubClass
对象,并使用getClass()
方法获取了它的类。然后我们使用getSuperclass()
方法获取了它的父类,并打印出了父类的名称。
3.4.方法的重写:
方法的重写是指子类可以重新定义父类中已经存在的方法。子类可以在重写方法中改变方法的实现方式,但方法的名称、参数列表和返回类型必须与父类中的方法一致。当调用重写的方法时,程序会根据实际对象的类型来决定调用哪个版本的方法,这就是多态性的体现。重写方法可以在子类中实现特定的行为,而不必改变父类的结构。
让我们假设有一个父类Animal,它有一个eat()方法:
public class Animal {
public void eat() {
System.out.println("The animal is eating");
}
}
现在我们创建一个子类Dog,它可以重写eat()方法:
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("The dog is eating");
}
}
在这个例子中,Dog类重写了父类Animal的eat()方法。当我们创建一个Dog对象并调用eat()方法时,将会调用Dog类中重写的eat()方法,输出"The dog is eating"。这就是方法的重写。
@Override
是一个注解,用于告诉编译器该方法是重写父类中的方法。在Java中,使用@Override
注解可以帮助程序员避免一些常见的错误,比如拼写错误或者不小心改变了方法签名。如果使用了@Override
注解,但实际上并没有重写父类中的方法,编译器会报错。这样可以帮助开发人员及时发现问题,并修复代码。
方法的重写需要遵循以下规则:
-
方法名必须与被重写的方法名相同。
-
参数列表必须与被重写的方法参数列表相同。
-
返回类型必须与被重写的方法返回类型相同,或者是其子类(只对引用类型有关)。
-
访问修饰符可以更宽松,但不能更严格。例如,如果父类的方法是public,则子类重写的方法可以是public或protected,但不能是private。
-
private父类无法被重写。
-
重写的方法不能抛出比被重写方法更多的异常,但可以不抛出异常或者抛出更少的异常。
-
静态方法不能被重写,因为静态方法与类相关联,而不是与对象相关联。
3.5.super
关键字:
在Java中,super
关键字用于引用父类的成员变量、方法或构造方法。它可以在子类中调用父类的构造方法、访问父类的成员变量或调用父类的方法。
举个例子,假设有一个父类Animal和一个子类Dog:
public class Animal {
String color = "white";
public void sleep() {
System.out.println("The animal is sleeping");
}
}
public class Dog extends Animal {
String breed = "Labrador";
public void display() {
System.out.println("Color: " + super.color); // 使用super关键字访问父类的成员变量
super.sleep(); // 使用super关键字调用父类的方法
}
}
在这个例子中,子类Dog中的display()
方法使用了super
关键字来访问父类Animal中的color
变量和sleep()
方法。这样就可以在子类中访问或调用父类的成员变量和方法。
除此之外,它还有以下作用:
super
与构造器的规则:
因此,当在子类中对父类的属性进行操作时,我们可以直接用属性名,它会先在子类中搜索,若不存在,则会在父类中寻找。
注意,调用的是父类的空参构造器!!!
常见错误分析:
//父类
public class man {
private int age;
private int name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getName() {
return name;
}
public void setName(int name) {
this.name = name;
}
public man(int age, int name) {
this.age = age;
this.name = name;
}
}
//子类
public class user extends man {
}
在这段代码中,存在一个问题:在user
类中没有显式定义任何构造器,而man
类中定义了一个带有参数的构造器。这会导致编译错误,因为当子类没有显式定义构造器时,它会默认调用父类的无参构造器。但是在这个情况下,man
类中并没有无参构造器,只有带参数的构造器。因此,编译器会报错,提示user
类无法调用父类的构造器。
为什么父类没有无参构造器:因为在有构造器时,系统不会默认给出一个无参构造器,而这个代码中父类有有参构造器了。
为了解决这个问题,可以在user
类中显式定义一个构造器,并在其中调用父类的构造器,或者在man
类中添加一个无参构造器。例如:
public class man {
private int age;
private int name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getName() {
return name;
}
public void setName(int name) {
this.name = name;
}
public man(int age, int name) {
this.age = age;
this.name = name;
}
public man() {
// 无参构造器
}
}
public class user extends man {
public user(int age, int name) {
super(age, name);
}
}
在这个修改后的代码中,user
类显式定义了一个构造器,并在其中调用了父类man
的带参数的构造器。或者,也可以在man
类中添加一个无参构造器,这样编译器就不会报错了。
修改方法有二:
-
确保父类有空参构造器,或者父类没有构造器,这样系统就会给出默认的空参构造器。
-
既然无论如何都会调用父类的构造器,且在没有显示的调用父类构造器时,会默认调用父类空参构造器,那么为什么不直接显示调用父类有参构造器呢,只要确保父类有有参构造器就行。
例题:
输出结果应该是:BAAB
。
输出结果应该是:ABAAB
。
与第一题不一样的是,本题有继承性,且没有显示调用构造器,则它会先调用父类空构造器,其他与第一题相同。
3.6.子类实例化的过程:
即使子类有父类,父类又有父类,但最终内存只开辟了一处空间,就是在new
关键字使用时开辟的空间。
4.多态性:
4.1.初识多态:
多态性是指在面向对象编程中,一个对象可以根据其所属的类的不同表现出不同的行为。具体来说,多态性包括两种形式:编译时多态性和运行时多态性。
编译时多态性是指在编译阶段确定对象的类型,而运行时多态性是指在运行阶段根据对象的实际类型确定调用的方法。
多态性可以通过继承和接口实现来实现,它能够提高代码的灵活性和可扩展性。在实际应用中,多态性可以通过方法的重写和重载来实现,从而实现不同类型的对象调用相同的方法产生不同的结果。
总之,编译看左边,运行看右边,但是,多态无法调用子类的方法。
举一个多态性简单的例子:
假设有一个动物类 Animal,它有一个方法 makeSound() 用于发出声音。然后有两个子类 Dog 和 Cat,它们分别重写了 makeSound() 方法。
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
public void makeSound() {
System.out.println("Woof");
}
}
class Cat extends Animal {
public void makeSound() {
System.out.println("Meow");
}
}
现在我们可以创建一个 Animal 类型的引用,然后根据具体的对象类型调用 makeSound() 方法,这样就实现了多态性:
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.makeSound(); // 输出 "Woof"
myCat.makeSound(); // 输出 "Meow"
在这个例子中,myDog 和 myCat 都是 Animal 类型的引用,但它们分别指向了 Dog 和 Cat 类型的对象。当调用 makeSound() 方法时,实际执行的是对应子类中重写的方法,这就是多态性的体现。
使用多态的前提:
-
有继承关系。
-
有方法重写。
多态的适应性:
适用于方法,不适用于属性。
当父类子类都有相同属性时,打印多态的值,此时,不满足于编译看左边,运行看右边,它的运行结果与父类有关。
4.2.多态的好处与弊端:
好处:
-
灵活性:多态性使得代码更加灵活,可以根据需要在运行时选择不同的实现。
-
可扩展性:通过多态性可以轻松地添加新的子类,而不需要修改现有的代码。
-
代码复用:可以使用父类类型的引用来引用子类对象,从而实现代码的复用。
弊端:
-
难以理解:对于初学者来说,多态性可能会增加代码的复杂性,使得代码难以理解和维护。
-
运行效率:在运行时需要确定对象的实际类型,可能会导致一定的性能损失。
-
难以调试:由于多态性的特性,可能会导致在调试时难以确定对象的真实类型,增加了调试的难度。
-
无法调用子类的方法。
举例说明:
好处:假设有一个图形类 Shape,它有一个方法 draw() 用于绘制图形。然后有多个子类,如 Circle、Rectangle、Triangle 等,它们分别重写了 draw() 方法。当需要画出不同的图形时,可以使用 Shape 类型的引用来引用不同的子类对象,从而实现了多态性,使得绘制图形的代码更加灵活和可扩展。
弊端:在某些情况下,由于多态性的特性,可能会导致在调试时难以确定对象的真实类型,增加了调试的难度。同时,由于需要在运行时确定对象的实际类型,可能会导致一定的性能损失。
4.3.虚方法调用:
虚方法调用是面向对象编程中的一个重要概念,它与多态性密切相关。在面向对象编程语言中,当一个父类的方法被子类重写时,如果通过父类类型的引用来引用子类对象并调用这个方法,那么在运行时会根据对象的实际类型来确定调用哪个方法,这就是虚方法调用。
在实现虚方法调用的过程中,编程语言会使用一种称为虚函数表(vtable)的机制来实现。每个对象都有一个指向其类的虚函数表的指针。当调用一个虚方法时,程序会根据对象的虚函数表找到对应的方法并调用。
虚方法调用的好处在于可以根据对象的实际类型来确定调用哪个方法,从而实现了多态性。这样就可以使用父类类型的引用来引用子类对象,并在运行时根据对象的实际类型来调用对应的方法,使得代码更加灵活和可扩展。
4.4.类型转化:
在Java中,多态性可以通过向上转型和向下转型来实现。
向上转型(Upcasting
):
是指将子类对象赋值给父类类型的引用变量。这样做是安全的,因为子类对象继承了父类的所有属性和方法,所以可以通过父类类型的引用来访问子类对象的属性和方法。向上转型可以实现多态性,使得代码更加灵活和可扩展。
示例:
class Animal {
public void makeSound() {
System.out.println("Animal makes sound");
}
}
class Dog extends Animal {
public void makeSound() {
System.out.println("Dog barks");
}
public void fetch() {
System.out.println("Dog is fetching");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // 向上转型
animal.makeSound(); // 调用的是 Dog 类的 makeSound 方法
// animal.fetch(); // 编译错误,无法调用子类特有的方法
}
}
在上面的示例中,我们将 Dog 对象赋值给 Animal 类型的引用变量 animal。尽管 animal 的类型是 Animal,但在运行时实际引用的是 Dog 对象,所以调用 makeSound
方法时实际上调用的是 Dog 类的 makeSound
方法,这就是多态性的体现。
向下转型(Downcasting
):
是指将父类类型的引用变量转换为子类类型的引用变量。这样做是有风险的,因为父类类型的引用变量可能并不真正引用一个子类对象。在进行向下转型时,需要使用 instanceof
运算符来检查对象的类型,以确保安全地进行转型。
示例:
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // 向上转型
if (animal instanceof Dog) {
Dog dog = (Dog) animal; // 向下转型
dog.fetch(); // 可以调用子类特有的方法
}
}
}
在上面的示例中,我们首先将 Dog 对象赋值给 Animal 类型的引用变量 animal,然后使用 instanceof
运算符检查 animal 的实际类型,如果是 Dog 类型,就进行向下转型并调用子类特有的方法 fetch。
注意,animal与dog的地址是一样的。
总之,向上转型和向下转型是实现多态性的重要手段,能够使得代码更加灵活和可扩展。但需要注意的是,在进行向下转型时需要进行类型检查,以确保安全地进行转型。
instanceof
:
a instanceof b:判断对象a是不是类b的实例
例如,上面的代码:animal instanceof Dog:
并不是判断animal
是不是Dog
的实例,而是判断Dog
是不是Dog
的实例,因为有Animal animal = new Dog();
,实际上是Dog
向上转型。
注意,如果a instanceof b
是true,那么a instanceof super.b
一定也是true;
例题:
Q:为什么b与s地址一样,但是打印的值不一样?
A:因为多态针对方法,不针对固有属性。
5.Object
类
Java中的Object类是所有类的根类,也就是说所有的类都直接或者间接的继承自Object类。Object类中包含了一些基本的方法,比如toString()
、equals()
、hashCode()
等,这些方法在其他类中都可以直接使用。
Object类中的方法有以下几种:
-
clone()
:用于创建并返回一个对象的拷贝,通常需要实现Cloneable
接口。 -
equals()
:用于判断两个对象是否相等。 -
hashCode()
:返回对象的哈希码值。 -
toString()
:返回对象的字符串表示。 -
getClass()
:返回对象的类。 -
notify()、notifyAll()、wait()
:用于线程间的通信。
由于所有的类都继承自Object类,因此Object类中的方法可以在所有的类中直接使用。在实际开发中,我们通常会重写Object类中的方法,以便更好地满足我们的需求。比如重写toString()方法可以返回对象的特定字符串表示,重写equals()方法可以自定义对象的相等判断逻辑等。
总的来说,Object类是Java中非常重要的一个类,它提供了一些基本的方法,为所有的类提供了一些共同的行为和特性。
5.1.equals()
:
在Java中,equals()
方法是Object类中的一个方法,用于比较两个对象是否相等。但是无法比较基本类型,在Object类中,equals()
方法的默认实现是使用==
运算符来比较两个对象的引用是否相等,即判断两个对象是否指向同一个内存地址。因此,当我们比较array,自定义类型时:
在Java中,如果你没有重写equals()
方法,那么这个方法将会比较对象的引用,而不是它们的内容。这意味着,即使两个数组或两个自定义类的对象包含完全相同的元素,equals()
方法也会返回false
,除非这两个引用指向的是同一个对象。
以下是一些示例:
// 比较数组
int[] array1 = {1, 2, 3};
int[] array2 = {1, 2, 3};
System.out.println(array1.equals(array2)); // 输出:false
// 比较自定义对象
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
Person person1 = new Person("John", 25);
Person person2 = new Person("John", 25);
System.out.println(person1.equals(person2)); // 输出:false
在上述示例中,尽管array1
和array2
包含相同的元素,person1
和person2
有相同的name
和age
,但是equals()
方法都返回了false
。这是因为equals()
方法在这种情况下比较的是对象的引用,而不是它们的内容。
如果你想要比较两个数组的内容,你可以使用Arrays.equals()
方法。如果你想要比较两个自定义类的对象的内容,你需要重写equals()
方法。
但是,我们可以重写equals()
,让它达到我们想要的结果,注意,在java
中,String,File,Date,包装等类型已经重写了equals()
:
// 比较字符串
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1.equals(str2)); // 输出:true
// 比较自定义对象
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
}
Person person1 = new Person("John", 25);
Person person2 = new Person("John", 25);
System.out.println(person1.equals(person2)); // 输出:true
自动重写:
在IDEA中,可以用AIt+insert实现自动重写:
5.2.tostring()
:
在Java中,toString()
方法是一个由Object
类继承而来的方法,用于返回对象的字符串表示。当我们需要将一个对象转换为字符串时,通常会调用toString()
方法。这个方法通常用于调试和日志记录,以便能够方便地查看对象的内容。
如果我们在自定义类中没有重写toString()
方法,那么它将使用Object
类中的默认实现,返回一个由类名和对象的哈希码组成的字符串。例如,对于一个自定义类Person
,如果没有重写toString()
方法,那么调用toString()
方法将返回类似于Person@1f7030a
这样的字符串。
重写tostring()
:
当你重写一个自定义类的toString
方法时,你可以根据你的需求来定义返回的字符串格式。例如,假设我们有一个Person
类,包含姓名和年龄属性,我们可以重写toString
方法来返回这些属性的字符串表示形式。
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 重写toString方法
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
public static void main(String[] args) {
Person person = new Person("Alice", 25);
System.out.println(person); // 输出:Person{name='Alice', age=25}
}
}
在这个例子中,我们重写了Person
类的toString
方法,将name
和age
属性的值以字符串的形式返回。当我们调用System.out.println(person)
时,会打印出Person{name='Alice', age=25}
。这样的重写使得打印对象时更容易理解对象的内容。
注意,重写to_String会影响print。
PART THREE:面向对象高级
1.static
关键字:
在Java中,static关键字可以用来修饰成员变量和成员方法,表示静态的、全局的、共享的。
1.静态变量:静态变量属于类,而不属于任何对象实例。它在类加载的时候就被初始化,并且所有的实例对象共享同一个静态变量。可以通过类名直接访问,也可以通过对象实例访问。未被static修饰的叫实例变量。
举例:
public class Example {
public static int count = 0;
public Example() {
count++;
}
public static void main(String[] args) {
Example e1 = new Example();
System.out.println(Example.count); // 输出1
Example e2 = new Example();
System.out.println(Example.count); // 输出2
}
}
2.静态方法:又叫类方法,静态方法属于类,不属于任何对象实例。它可以直接通过类名调用,不需要实例化对象。静态方法不能直接访问非静态成员变量和非静态成员方法,只能访问静态成员变量和静态成员方法。
举例:
public class MathUtil {
public static int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(MathUtil.add(2, 3)); // 输出5
}
}
在static修饰的方法内,不能使用super和this。
总之,static关键字的使用可以提高程序的执行效率,减少不必要的内存开销,但在使用时需要慎重考虑,因为static成员是全局共享的,可能会带来一些潜在的问题。
2.单例设计模式:
是一种创建模式,用于确保一个类只有一个实例,并提供一个全局访问点。
单例设计模式通常包括以下几个要素:
-
私有构造函数:确保其他类无法直接实例化该类。
-
静态变量:用于保存该类的唯一实例。
-
静态方法:用于获取该类的唯一实例。
单例设计模式的优点包括:
-
节省资源:由于只有一个实例,可以节省内存和其他资源。
-
全局访问点:可以方便地访问该类的唯一实例。
-
避免重复实例化:可以避免多次实例化该类,确保全局唯一性。
然而,单例设计模式也有一些缺点,包括:
-
破坏封装性:全局访问点可能导致其他类直接修改单例实例,破坏了封装性。
-
线程安全性:在多线程环境下,需要额外考虑单例实例的线程安全性。
总的来说,单例设计模式是一种常用的设计模式,可以确保某个类只有一个实例,并提供全局访问点,但需要注意线程安全和封装性等问题。
饿汉式和懒汉式都是单例模式的实现方式,用于确保一个类只有一个实例。
饿汉式: 在类加载的时候就创建实例,所以称为"饿汉式"。这意味着无论是否需要使用该实例,它都会被创建。这种方式简单且线程安全,但可能会导致资源浪费。
示例代码:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
懒汉式: 在需要时才创建实例,所以称为"懒汉式"。懒汉式在多线程环境下需要考虑线程安全性,通常需要使用同步机制来确保只创建一个实例。
示例代码:
public class Singleton {
private Singleton() {}//先私有构造器
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
在懒汉式中,需要时才会new对象,但这也会导致性能下降。
总的来说,饿汉式在类加载时就创建实例,简单且线程安全,但可能会导致资源浪费。懒汉式在需要时才创建实例,需要考虑线程安全性,通常会导致性能下降。选择哪种方式取决于具体的需求和场景。
注意:
-
无论是那种方法,都要私有构造器。
-
无论哪种方法,都要static变量,确保只有一份。
-
无论哪种方法,方法都必须是static,这样才能不创建对象就调用方法,防止闭环。
3.代码块:
在Java中,代码块是由一对大括号{}包围的一段代码,它可以包含一组语句,可以是方法内部的局部代码块,也可以是类的静态代码块或构造代码块。
注意,代码块只能被static修饰:
方法内部的局部代码块:
public class CodeBlockExample {
public static void main(String[] args) {
int a = 10;
{
int b = 20;
System.out.println("a + b = " + (a + b));
}
// 这里无法访问变量b,因为b的作用域仅限于局部代码块内
// System.out.println("a + b = " + (a + b)); // 编译报错
}
}
静态代码块:
public class CodeBlockExample {
static {
System.out.println("这是静态代码块,会在类加载时执行");
}
public static void main(String[] args) {
// 静态代码块会在main方法之前执行
}
}
构造代码块:
public class CodeBlockExample {
{
System.out.println("这是构造代码块,会在构造方法执行前执行");
}
public CodeBlockExample() {
System.out.println("这是构造方法");
}
public static void main(String[] args) {
CodeBlockExample example = new CodeBlockExample();
// 构造代码块会在构造方法执行前执行
}
}
代码块可以用来初始化变量、执行一些特定的逻辑或者在对象创建时执行一些初始化操作。它们可以帮助我们更好地组织代码,提高代码的可读性和可维护性。
对赋值的补充:
代码块的加载时有:由父及子,静态先。
4.final:
在Java中,final关键字有以下几种用法:
修饰类:当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的所有成员方法都会被隐式地指定为final方法。
修饰方法:final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。因此,如果只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为final。总之,可以防止方法被重写。
修饰变量:对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象,但是它的成员可以变。
对于修饰变量来说:我们可以在创建时不对它进行初始化,这样我们在后面可以对它进行一次的修改。对于它修饰成员变量,那么以下三个方法对它赋值时有效的,用方法给它赋值是无效的:
-
显式赋值
-
代码块赋值
-
构造器赋值
这里有一个例子来说明final关键字的用法:
public class Test {
public static void main(String[] args) {
final int i = 10; // 声明一个final变量i
// i = 20; // 错误: 无法为最终变量i分配值
final MyClass myClass = new MyClass();
// myClass = new MyClass(); // 错误: 无法为最终变量myClass分配值
System.out.println(++myClass.j); // 输出: 1
}
}
class MyClass {
public int j = 0;
}
在这个例子中,final变量i和myClass在初始化后就不能再改变,但是myClass指向的对象的内容是可以改变的1。
当final与static一起使用,表示全局常量。
5.抽象性:
情景导入:
在Java中,抽象是一种面向对象编程的关键概念,它允许我们创建不能实例化的类和不能完全实现的方法。这些类被称为抽象类,这些方法被称为抽象方法。
抽象类是一种特殊的类,它不能被实例化,只能被继承。抽象类可以包含抽象方法和非抽象方法。
抽象方法是一种没有实现的方法,它只有声明没有具体的实现。子类必须提供抽象方法的实现。
以下是一个Java抽象类和抽象方法的例子:
abstract class Animal {
// 抽象方法
abstract void makeSound();
// 非抽象方法
public void eat() {
System.out.println("The animal eats");
}
}
class Dog extends Animal {
// 提供抽象方法的实现
void makeSound() {
System.out.println("The dog barks");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建Dog对象
dog.makeSound(); // 调用抽象方法的实现
dog.eat(); // 调用非抽象方法
}
}
在这个例子中,Animal
是一个抽象类,它有一个抽象方法makeSound()
和一个非抽象方法eat()
。Dog
类继承了Animal
类,并提供了makeSound()
方法的实现。在main
方法中,我们创建了一个Dog
对象,并调用了makeSound()
和eat()
方法。这就是Java中抽象的基本用法。
注意,抽象类由构造器,因为子类初始化会调用父类构造器。并且,子类必须重写父类所有的抽象方法,否则,它也是抽象类,无法实例化。
abstract不能使用的对象:
-
属性,构造器,代码块。
-
私有方法,因为私有方法无法被重写。
-
静态方法,因为静态方法可以不用实例化就调用,而abstract就是不让实例化,影响不了静态方法。
-
final类及其方法,因为final方法不能被重写,final类不能被继承。
6.接口:
在Java编程语言中,接口是一种引用类型,就像类一样,但是它只包含了常量(所有字段都自动为public static final)和抽象方法(所有方法都自动为public abstract)。接口不能包含构造器,也不能包含已经实现的方法。因为所有字段都自动为public static final,所有方法都自动为public abstract,所以应用时也可以省略掉该关键字。
在Java中,接口是一种完全抽象的类型,它允许我们定义一组方法,但不提供这些方法的实现。这些方法必须由实现接口的类来提供。接口的主要用途是确保遵循某种规范。一个类可以实现一个或多个接口,这意味着它提供了接口中所有方法的实现。
以下是一个Java接口的例子:
interface Animal {
void makeSound(); // 接口方法(抽象方法)
}
class Dog implements Animal {
// 提供接口方法的实现
public void makeSound() {
System.out.println("The dog barks");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建Dog对象
dog.makeSound(); // 调用接口方法的实现
}
}
在这个例子中,Animal
是一个接口,它有一个方法makeSound()
。Dog
类实现了Animal
接口,并提供了makeSound()
方法的实现。在main
方法中,我们创建了一个Dog
对象,并调用了makeSound()
方法。这就是Java中接口的基本用法。
接口多态性的四种体现:
USB为接口,Computer接受一个USB类型的参数,而Printer,Camera都是接口的实现类,而用USB类型来接收一个Printer类型,其实本身就是多态性的体现:USB usb1 = new printer();
在实际中,我们可能传入任意的USB的实现类,因此我们可以就用USB类型来接受,都是因为接口没有构造函数,而且必须重写方法,所以我们需要在后面重写接口的方法。
我们也可以用匿名对象的方法传参,道理与三相同。
快捷操作:
在IEDA中,给接口写实现类时,可以用AIt+Enter快捷操作,系统会自动写入:
JDK8的改变:
在JDK8中,接口中方法不在局限于抽象方法,而是增加了默认方法与静态方法:
但有几点需要注意:
-
对于静态方法,只能被接口调用,不能被它的实现类调用。
-
默认方法可以被实现类继承,重写覆盖。
-
类实现了两个接口,而两个接口中定义了同名同参数的默认方法,此时会接口冲突,因此,必须重写接口,覆盖掉两种默认方法。
-
类优先原则,如果一个类继承了父类并且作为接口的实现类,且父类与接口中定义了同名同参数的默认方法,此时不会报错,因为默认调用的是父类的方法。
-
父类或者接口有默认方法,在子类和实现类中要想调用父类或者接口的被重写前的方法,可以用super进行操作,比如:接口名字叫A,那么就可以用A.super.method(),调用被重写前的默认方法。
-
在接口中定义默认方法时要加上
default
。
7.内部类:
Java的内部类是定义在另一个类或方法中的类。根据使用场景的不同,内部类可以分为四种:成员内部类,局部内部类,匿名内部类和静态内部类。
public class Outer {
public class Inner {
}
}
成员内部类可以被权限修饰符修饰,可以访问外部类的所有成员,包括private成员。它不能定义static成员。
public class Outer {
public void test() {
class Inner {
}
}
}
局部内部类不能有访问权限修饰符,也不能被定义为static。
public class Outer {
public List list = new ArrayList() {
{
add("test");
}
};
}
public class Outer {
public static class Inner {
}
}
嵌套类是四种类中唯一一个不包含对外部类对象的引用的内部类。
总之,一四都是成员内部类,二三都是局部内部类。
7.1.成员内部类
成员内部类是定义在类内部,作为类的成员的类。它可以被权限修饰符修饰,如public、private等。成员内部类可以访问外部类的所有成员,包括private成员。但是,如果它是非静态内部类,则它不能定义static成员。这是因为静态变量是可以不被实例化就调用的,而静态变量的类必须是静态的。
下面是一个成员内部类的例子:
public class Outer {
private int outerVariable = 1;
public class Inner {
public void innerShow() {
// 访问外部类的成员变量
System.out.println("outerVariable: " + outerVariable);
}
}
}
在这个例子中,Inner是一个成员内部类,它可以访问外部类Outer的私有成员变量outerVariable。
要创建成员内部类的实例,需要先创建外部类的实例,然后通过外部类的实例来创建内部类的实例。例如:
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.innerShow();
//如果是静态的成员内部类,或者也可以这样
Outer.Inner inner = new.Outer.Inner();
在这段代码中,我们首先创建了一个Outer类的实例outer,然后通过outer.new Inner()来创建了一个Inner类的实例inner,最后调用inner.innerShow()来打印外部类的成员变量outerVariable
成员内部类的调用:
在内部类调用内部类的:省略+调用对象或者this.+调用对象
在内部类调用外部类的:外部类名字.this.+调用对象
7.2.局部内部类:
局部内部类是定义在外部类的局部位置,比如方法或代码块中,并且有类名。它可以直接访问外部类的所有成员,包括私有的1。局部内部类的地位相当于一个局部变量,因此不能添加访问修饰符,但可以使用final进行修饰1。局部内部类的作用域仅仅在定义它的方法或代码块中有效。
下面是一个局部内部类的例子:
public class OuterClass {
public void someMethod() {
class LocalInnerClass {
public void printMessage() {
System.out.println("Hello from the local inner class!");
}
}
LocalInnerClass localInnerClass = new LocalInnerClass();
localInnerClass.printMessage();
}
}
在这个例子中,LocalInnerClass是一个局部内部类,它被定义在someMethod方法中。我们在方法中创建了LocalInnerClass的一个实例,并调用了它的printMessage方法。
开发中的四种场景(可以参照上面接口的四种多态性):
方式一:提供了接口实现类的对象:
public class user {
public Comparable getInstance(){
class MyComparable implements Comparable{
@Override
public int compareTo(Object o) {
return 0;
}
}
MyComparable m = new MyComparable();
return m;
}
}
在getInstance
方法中,定义了一个局部内部类MyComparable
,这个类实现了Comparable
接口。我们通过这个内部类得到一个返回值为实现该接口的对象。
方式二:提供了接口实现类的匿名对象:
public class user {
public Comparable getInstance(){
class MyComparable implements Comparable{
@Override
public int compareTo(Object o) {
return 0;
}
}
return new MyComparable();
}
}
方式三:提供了接口匿名实现类的对象:
方式四:提供了接口匿名实现类的匿名对象:
注意,看起来三四没有类,但其实有匿名的类。
7.3.难点:提供继承于某类的匿名子类:
举例:提供继承于Object类的匿名子类的对象:
注意,要先区分子类与对象,比如有一个类为Fly类,继承于它的类有bird类,sparrow类,这是它的子类,对象是子类的实例,要有new关键字的引用,而什么是匿名子类,就表示这个类没有固定的名字,因此,我们可以这样:
Object obj = new Object() {
// 在这里添加你需要的方法或字段
public void myMethod() {
System.out.println("Hello from myMethod");
}
};
// 由于obj的静态类型是Object,我们需要进行类型转换才能调用myMethod
((MyObject)obj).myMethod();
obj就是对象,new Object()
是匿名对象,但是注意我们需要进行类型转换才能调用重写的代码,向上面那样的方法肯定是不行的,因为我们不知道MyObject
是什么,它是匿名的,所以我们应该:
new Object() {
// 在这里添加你需要的方法或字段
public void myMethod() {
System.out.println("Hello from myMethod");
}
}.myMethod();
抽象类也可以用这样的办法。
8.枚举类:
在Java中,枚举(Enum)是一种特殊的类,它用于定义固定数量的常量。枚举可以包含字段、方法和构造函数,就像普通的Java类一样。
以下是一个简单的枚举类的例子:
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
在这个例子中,Day
是一个枚举类,它定义了一周的七天。你可以像使用其他Java类一样使用枚举类。例如,你可以在switch语句中使用枚举,或者在方法中使用枚举作为参数或返回类型。
枚举类还有一些特殊的方法,如values()
方法返回枚举类的所有值的数组,valueOf(String name)
方法返回给定名称的枚举常量。
以下是一个使用枚举类的例子:
public class Main {
public static void main(String[] args) {
Day day = Day.MONDAY;
switch (day) {
case MONDAY:
System.out.println("It's Monday.");
break;
// 其他case省略
default:
System.out.println("Invalid day.");
}
}
}
在这个例子中,我们创建了一个Day
类型的变量day
,并在switch语句中使用它。
8.1.格式以及注意事项:
在Java中,枚举(Enum)的基本格式如下:
public enum EnumName {
CONSTANT1, CONSTANT2, CONSTANT3, ...;
}
其中,EnumName
是枚举的名称,CONSTANT1
、CONSTANT2
、CONSTANT3
等是枚举的常量。这些常量在定义时都会自动初始化为EnumName
类型的对象。
在使用枚举时,有一些需要注意的事项:
-
常量名称通常大写:枚举常量通常使用全大写字母,这是Java编程的一种惯例。
-
枚举可以有构造函数、字段和方法:你可以在枚举中定义构造函数、字段和方法,就像在普通的Java类中一样。但是,枚举的构造函数默认是私有的,你不能从枚举外部调用它。
-
枚举常量是单例:每个枚举常量在程序运行期间只有一个实例,所以你可以安全地使用
==
来比较两个枚举常量。 -
枚举可以实现接口:枚举可以实现一个或多个接口,但不能继承其他类,因为枚举已经隐式地继承了
java.lang.Enum
类。 -
枚举可以用在switch语句中:你可以在switch语句中使用枚举常量,这是枚举的一个常见用途。
-
在Java中,枚举类的变量(也就是枚举常量)可以直接使用,是因为它们在定义时就已经被实例化了。
以下是一个包含构造函数、字段和方法的枚举的例子:
public enum Day {
MONDAY("Monday"), TUESDAY("Tuesday"), WEDNESDAY("Wednesday"),
THURSDAY("Thursday"), FRIDAY("Friday"), SATURDAY("Saturday"), SUNDAY("Sunday");
private String dayName;
Day(String dayName) {
this.dayName = dayName;
}
public String getDayName() {
return this.dayName;
}
}
在这个例子中,Day
枚举有一个私有字段dayName
,一个构造函数和一个公有方法getDayName()
。每个枚举常量在定义时都会调用构造函数来初始化dayName
字段。
8.2.枚举类实现接口:
枚举类也是类,虽然无法显式继承,但是可以实现接口。
情况一:自己在枚举类中公共部分写重写后的部分就行。
情况二:某个枚举属性分别重写:
9.Annotation:
在Java中,注解(Annotation)是一种用于提供元数据的机制。元数据是关于数据的数据,它可以用于描述类、方法、变量、参数和包等程序元素。
注解的主要用途包括:
- 编译器指导:注解可以被编译器用来检测错误或警告。
- 编译时和部署时代码处理:软件工具可以通过注解来生成代码、XML文件等。
- 运行时处理:某些注解可以在运行时被读取,并用于执行某些功能。
Java内置了一些预定义的注解,例如@Override
、@Deprecated
、@SuppressWarnings
等。你也可以定义自己的注解,这被称为自定义注解。
以下是一个自定义注解的例子:
public @interface MyAnnotation {
String value() default "";
}
在这个例子中,我们定义了一个名为MyAnnotation
的注解,它有一个名为value
的元素。
你可以像这样使用这个注解:
@MyAnnotation(value = "Hello")
public class MyClass {
// ...
}
在这个例子中,我们在MyClass
类上使用了MyAnnotation
注解,并设置了value
元素的值为"Hello"
。
总的来说,注解是Java提供的一种强大的元数据处理机制,它在许多Java框架(如Spring、Hibernate等)中都有广泛的应用。
元注解
单元测试:
Java单元测试是指对Java程序中的各个独立单元(如方法、类等)进行测试的过程。它旨在验证每个单元的功能是否符合预期,以确保程序的正确性和稳定性。
在Java中,常用的单元测试框架包括JUnit和TestNG。通过这些框架,开发人员可以编写测试用例,并使用断言语句来验证单元的输出是否符合预期。
单元测试的好处包括:
-
提高代码质量:通过测试用例覆盖各种情况,可以发现潜在的bug和逻辑错误,从而提高代码的质量。
-
便于重构:在重构代码时,可以通过运行单元测试来验证代码的功能是否受到影响。
-
提高开发效率:及早发现问题,减少调试时间,提高开发效率。
总之,Java单元测试是编写高质量、稳定的程序的重要手段,是现代软件开发过程中不可或缺的一部分。
当使用JUnit进行单元测试时,可以使用@Test
注解来标记测试方法。以下是一个简单的示例:
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CalculatorTest {
@Test
public void testAdd() {
Calculator calculator = new Calculator();
int result = calculator.add(3, 5);
assertEquals(8, result);
}
}
在这个示例中,我们创建了一个名为CalculatorTest
的测试类,并使用@Test
注解来标记testAdd
方法。在testAdd
方法中,我们创建了一个Calculator
对象,并调用其add
方法来进行加法运算。然后使用assertEquals
方法来验证计算结果是否符合预期值。
这个例子展示了如何使用@Test
注解进行简单的单元测试,通过断言语句来验证程序的输出是否符合预期。这样可以确保Calculator
类中的add
方法能够正确地执行加法运算。注意,此时可以不在main方法中进行运行。
注意:在单元测试中使用scanner会失效,需要一些操作。
模板设置:
在idea中,可以设置test的模板,这样输入指定关键词,就会自动写出该模板:
10.包装类:
包装类是一种用来包装基本数据类型的类。在Java中,每种基本数据类型都有对应的包装类,如Integer对应int,Double对应double等。包装类提供了一些方法来操作基本数据类型,以及在需要使用对象的情况下,可以将基本数据类型转换为对象。例如,可以使用包装类来在集合类中存储基本数据类型,或者在使用反射时需要操作基本数据类型的对象。常见的包装类还提供了一些静态方法来进行数据类型转换和数学运算。
10.1.基本数据类型->包装类:
1.使用包装类的构造器:
注意,在使用构造器时,我们不一定要传入一样的类型,比如:
但是字符串内部必须是相应类型的数,不过对于boolean类型,即使是False也可以识别。
2.推荐使用valueof:
10.2.包装类转回基本数据类型:
1.调用xxxvalue:
注意,在实际运用中,不要把大小写写错了,一个默认值为为null,一个为其他的默认值。
10.3.新特性:
10.3.1.自动装箱:
10.3.2.自动拆箱:
11.String与包装类,基本数据类型的转换:
11.1.基本数据类型,包装类---->String:
1.
2.
String str = 10 + "";