一 面向对象三大特性:
面向对象编程(Object-Oriented Programming,OOP)的三大特性分别是封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。
-
封装(Encapsulation):
- 封装是将对象的状态和行为(数据和方法)封装在对象内部,隐藏对象的内部实现细节,只暴露必要的接口供外部访问。
- 封装可以保护对象的数据不被意外修改,同时也提供了更好的数据安全性和灵活性。通过封装,对象的内部实现可以被更轻松地修改,而不会影响到外部代码。
-
继承(Inheritance):
- 继承是指一个类(子类)可以继承另一个类(父类)的属性和方法。子类可以重用父类的代码,并且可以在其基础上进行扩展或修改。
- 继承可以建立类之间的层次关系,提高代码的复用性和可维护性。子类可以通过继承获得父类的属性和行为,同时可以通过重写父类方法来实现多态。
-
多态(Polymorphism):
- 多态是指同一操作作用在不同的对象上可以有不同的行为。对象的多态性使得一个方法可以处理不同类型的对象,从而提高了代码的灵活性和可扩展性。
- 多态包括编译时多态性和运行时多态性。编译时多态性是通过方法重载实现的,根据参数的不同来调用不同的方法;而运行时多态性是通过继承和方法重写实现的,根据对象的实际类型来确定调用的方法。
- 总结:编译时多态:方法的重载 运行时多态:方法的重写和向上转型(父类 对象名=new 子类();
这三大特性是面向对象编程的核心概念,通过封装、继承和多态,可以更好地组织和管理代码,提高代码的可维护性、可扩展性和重用性。
二 局部代码块和构造代码块
代码块就是在代码中使用{ }括起来的代码部分就是方法块,代码块:在程序中用{ }括起来的代码部分。
1.构造代码块
构造代码块是指在类中定义的一段代码片段(在类中,不在方法中),在每次创建对象时都会被执行。构造代码块在类的构造函数执行之前执行,可以用来初始化对象的成员变量或执行其他必要的操作。构造代码块也用一对大括号{ }包围,放在类的成员位置。
class Student{
//构造代码块
{
System.out.println("构造代码块实现了");
}
}
2.局部代码块
局部代码块是指在方法中定义的一段代码片段(定义在类中的方法中的代码块,不是定义在类中),通常用于限定变量的作用范围,提高代码的可读性和可维护性。局部代码块用一对大括号{ }包围,其中可以包含多条语句。局部代码块:局部的代码块称为局部代码块,类似于局部变量。
class Student {
public void test(){
//局部代码块
{
System.out.println("局部代码块实现了");
}
}
}
三 super关键字
super是作为一个父类对象的关键字,可以通过super关键字来操作父类成员变量和方法
用法:super.成员变量:调用父类对象的成员变量。
super():调用父类的构造方法。
每一个构造方法中都会默认有一个super()
super必须在有效代码的第一行。
super.成员方法():调用父类对象的成员方法。
四 重写,重载和向上转型(都属于多态)
重写和重载的区别:
重写(Overriding)和重载(Overloading)是面向对象编程中常用的两种概念,它们有以下几点区别:
-
重写(Overriding):
- 重写是指子类重新定义父类中已有的方法,通过继承关系来实现。子类中的方法名、参数列表和返回类型必须与父类中被重写的方法相同。
- 重写关注的是继承关系中的方法的覆盖,子类通过重写父类方法来改变方法的行为。
- 重写实现的是运行时多态性,即在运行时根据对象的实际类型来确定调用的方法。
-
重载(Overloading):
- 重载是指在同一个类中,可以定义多个同名方法,但参数列表不同(参数类型、参数个数或参数顺序不同)。不同的方法根据传入的参数来确定调用哪个方法。
- 重载关注的是同一个类中方法的多态性,同名方法在同一个类中可以有不同的参数表,但方法名相同、参数列表不同。
- 重载实现的是编译时多态性,即在编译时根据方法的参数列表来确定调用的方法。
总的来说,重写是发生在继承关系中,父类和子类之间的方法重定义;而重载是在同一个类中,同名方法根据参数列表的不同来区分。重写是运行时多态性,重载是编译时多态性。在实际开发中,重写和重载常常同时使用,能够提高代码的灵活性和可扩展性。
方法重写的原则:
1.方法签名一致,即方法的名称,返回值,修饰符,形参等等全部相同
2.如果父类的方法的返回值类型是基本数据类型/void,那么子类在重写该方法时也要保持相同的返回值类型。
3.子类重写的方法的范围修饰符要大于或等于父类的该方法的修饰符
class A{
//父类方法的权限修饰符
protected void m(){}
}
class B extends A{
//继承的子类的权限修饰符
public/protected void m(){}
}
4.如果父类的方法的返回值类型是应用数据类型的话,那么子类的重写的方法的返回值类型,要么与父类的方法的返回值类型相同,要么是父类的方法的返回值的类型的子类。
class A{}
class B extends A{}
//B是C的父类
class C {
public A m(){
return null;
}
}
//C是D的父类
class D extends C{
public A/B m(){
return null;
}
}
方法重载的规则如下
- 方法名称必须相同。
- 方法参数列表必须不同,包括参数的类型、顺序或数量。
- 方法的返回类型可以相同也可以不同,只要参数列表不同即可。
- 方法的访问修饰符可以相同也可以不同。
- 方法的异常声明可以相同也可以不同。
根据上述规则,当在同一个类中定义了多个方法,它们的方法名相同但是参数列表不同,就构成了方法的重载。Java编译器会根据不同的参数列表来确定调用哪个重载的方法。方法重载可以使代码更清晰、模块化,并提高代码的可读性和灵活性。
向上转型
格式:父类 对象名=new 子类();
使用向上转型创建对象的时候,编译期间会检查声明类和创建类之间是否有继承关系,只要有继承关系就可以编译通过,但是并不关心具体是哪一个子类,只有等到运行的时候才会察看是哪一个具体的子类
十字诤言:编译看左边,运行看右边。左边时声明类,右边是实现类,即正真的实际类型。
解释:这个对象是用父类声明的,但是具体实现却是由子类实现的,所以具体的方法应该为子类中的方法。但是由于是由父类声明的,所以在编译时会出现使用不了子类特有的属性和方法,但是它时实际存在的,解决办法就是再用子类重新声明一个对象,然后用这个新的对象去接收该对象从而调用方法(期间需要使用强制类型转换又称向下转换)
例子如下:
public class Main {
public static void main(String[] args) {
Animal dog=new Dog();
//如果直接dog.eat()调用方法会出错
Dog d=(Dog)dog;
d.eat();
d.sleep();
}
}
class Animal{}
class Dog extends Animal{
public void eat(){
System.out.println("dag eat");
}
public void sleep(){
System.out.println("dog sleep");
}
}
向上转型的解耦
向上转型是指将子类对象赋值给父类对象的过程,这样可以实现多态性。解耦是指降低类之间的依赖性,使得它们之间的耦合度更低,从而提高代码的灵活性和可维护性。向上转型可以实现解耦的效果,因为当使用父类类型引用子类对象时,代码只依赖于父类接口或抽象类,而不依赖于具体的子类实现。这样,当需要替换子类时,只需要修改创建对象的地方,而不需要修改其他使用对象的地方。这种解耦的方式使得代码更易于扩展和维护。
五 static关键字
static是一个关键字,也是一个修饰符,可以修饰数据,方法,代码块,内部类。被static修饰的内容时属于类的,而不是属于类中的方法的。在调用变量或者方法时使用的是 类名.方法/变量 的形式
1. 静态变量
static关键字修饰的变量成为静态变量/类变量。静态变量是伴随着类的加载从而加载到方法区的,在方法区中被赋予默认值,静态变量的产生是先于对象的,即在对象产生之前,静态变量就存在了。该类产生的所有对象都共用同一个静态变量,每一个对象储存的都是静态变量的地址,无论在哪一个对象中修建该静态变量的值,旧值都会覆盖,之后静态变量中储存的都是更改后的值。
注意:
1.类是加载到方法区的
2.类是在第一次使用时才加载到方法区(创建实例对象时,即new关键字使用)
3.类只加载到方法区一次
4.静态变量不能定义在构造方法中,因为静态变量是在类加载的时候就初始化了,而构造方法则是在创建对象的时候才执行。即构造方法的执行顺序是在静态变量之后的。
2.静态方法
在类中被static修饰的方法称为静态方法,静态方法在类加载的时候就会被加载到方法区,此时只是加载但是并没有区执行,只是储存在方法区,在需要调用的时候通过类名来使其调用到栈内存中执行(因为静态方法也是先于对象存在的,所以也通过类名调用。
注意
1.静态变量中不能使用this,super,因为this和super代表的是对象,静态方法是先于对象出现的。
2.静态方法中不能调用本类的非静态方法,因为静态方法中不能使用this,加载静态方法时对象还未创建。
3.静态方法可以被继承,但是不能被重写或隐藏。
3.静态代码块
静态代码块就是用static{}修饰的代码块,静态代码只在类加载的时候执行一次,静态代码块实在栈内存总执行。
六 执行顺序问题
父类静态->子类静态->父类非静态->子类非静态
注意
先进行类加载,类加载完成以后才进行构造方法和构造代码块的调用,其中如果有多个静态内容按照代码的编写顺序区执行
class A{
static {
i+=5;
}
static int i=3;
}
这个代码是会编译失败的,因为静态变量在没有执行赋值语句之前是不可以进行操作的,比如加减乘除等操作,其他顺序是可以的
例如
class A{
static {
i=5;
}
static int i=3;
//或者static i;
}
class A{
static int i=3;
static {
i=5;
}
}
这些都是可以通过的,因为在准备阶段都会把静态变量i放入方法区并且将标记默认值设为0,之后在初始化阶段会执行标记值的修改和赋值语句,但是静态变量在没有执行赋值语句之前是不可以进行操作的,比如加减乘除等操作,因为在赋值语句之前i只是有一个标记值,并不是它真正的值。
在Java中,静态变量在类加载的准备阶段会被赋予默认值(比如int类型默认值为0),并且会在方法区中分配内存空间。在类加载的初始化阶段,静态变量会被赋予实际的初始值,这时才可以进行加减乘除等操作。在赋值语句执行之前,静态变量的值只是一个默认值,并不能进行操作。
在准备阶段,静态变量的赋值是通过静态变量的字节码指令来进行的,而在初始化阶段则是执行静态变量的赋值语句。因此,在赋值语句之前,静态变量的值可能是默认值,无法进行实际的操作。
七 final关键字
final :修饰符,用来修饰数据,方法,类
1.final常量
final修饰的数据称之为常量,不能被再次修改。(下面两种数据类型都是数据的一部分)
常量:在命名时全部字母一般都要用大写(规定俗称)
final修饰的基本数据类型,其值不能改变。
final int i=1;
i=10;//编译不通过,被final修饰的基本数据类型的值不能被更改
final修饰的引用数据类型,其地址不能被改变。
final int arr[]={2,3,4,5,6,8};
arr[0]=10;//编译通过
arr=new int[3];//编译失败,地址发生改变
arr=Arrays.copyof(arr,arr.length*2)//编译失败,地址发生改变
成员常量要求在对象被创建好之前就完成赋值,所以可以直接赋值,也可以在构造方法或者构造代码块中进行赋值,在构造方法中赋值时需要注意在每一个构造方法中都需要对成员常量进行赋值。
如果是静态成员常量,那么只能直接赋值或者在静态代码块中赋值
2.final类
final类不能有子类,无法被继承,(俗称断子绝孙类)。
3.final方法
被final修饰的方法称为最终方法,不能被重写或者隐藏,因为重写和隐藏就是在改变该方法的内容。但是final方法可以被继承和重载,重载是重新写一个方法,并不是改变之前的方法,而继承则是在子类中引用父类方法,也不存在方法的改变。
八 abstract关键字
在Java中,abstract
关键字用于定义抽象类和抽象方法。下面是关于abstract
关键字的详细介绍:
-
抽象类(Abstract Class):抽象类是用
abstract
关键字声明的类,它不能被实例化。抽象类通常用来定义一些通用的属性和方法,但是其中可能包含一些抽象方法,即没有具体实现的方法。如果一个类包含至少一个抽象方法,那么该类必须声明为抽象类。 -
抽象方法(Abstract Method):抽象方法是用
abstract
关键字声明的方法,它没有具体的实现。抽象方法只有方法的声明部分,而没有方法体。任何包含抽象方法的类都必须声明为抽象类。抽象方法的具体实现由子类来完成。 -
抽象类的特点:
- 抽象类不能被实例化,只能用作父类,被子类继承。
- 抽象类中可以包含抽象方法和非抽象方法。
- 子类继承抽象类时,必须实现其中的所有抽象方法,除非子类也声明为抽象类。
- 如果一个非抽象类继承了一个抽象类,那么它必须实现父类的抽象方法。
示例代码:
abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public abstract void eat();
public void sleep() {
System.out.println(name + " is sleeping");
}
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(name + " is eating");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog("Buddy");
dog.eat(); // Output: Buddy is eating
dog.sleep(); // Output: Buddy is sleeping
}
}
在上面的示例中,Animal
是一个抽象类,其中包含一个抽象方法eat()
和一个非抽象方法sleep()
。子类Dog
继承了Animal
类并实现了eat()
方法。
为什么一般使用抽象类会用向上转型来创建对象?
答:
一般使用抽象类时会使用向上转型来创建对象,主要是因为抽象类不能被实例化,只能用作父类,被子类继承。向上转型指的是将子类的实例赋值给父类类型的引用变量。这样做有以下几个好处:
-
多态性(Polymorphism):通过向上转型创建对象,可以实现多态性。多态性是指在程序运行时,同一个方法调用可以在不同对象上表现出不同的行为。因为父类引用指向子类对象,可以通过父类引用调用子类重写的方法,实现不同子类对象的不同行为。
-
灵活性和扩展性:使用向上转型创建对象使得程序更加灵活和可扩展。如果后续需要添加新的子类,只需要修改对象实例化的地方,而不需要修改其他代码。这样可以减少代码的耦合度,提高系统的灵活性和扩展性。
-
统一接口:通过向上转型创建对象,可以统一接口。父类引用可以引用不同子类对象,但对外提供的接口是一致的。这样在接口设计上更加统一,易于管理和维护。
示例代码:
abstract class Shape {
public abstract void draw();
}
class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
public class Main {
public static void main(String[] args) {
Shape circle = new Circle(); // 向上转型创建Circle对象
Shape rectangle = new Rectangle(); // 向上转型创建Rectangle对象
circle.draw(); // Output: Drawing a circle
rectangle.draw(); // Output: Drawing a rectangle
}
}
在上面的示例中,Shape
是一个抽象类,其中定义了一个抽象方法draw()
。通过向上转型创建Circle
和Rectangle
对象,可以统一使用Shape
类型的引用来调用各自子类实现的draw()
方法。