目录
什么是面向对象
面向对象是一种编程思想的体现. 一门语言发明之前,发明者就已经决定了它是否具备面向对象的特点.
面向对象所对应的是面向过程.
面向过程 解决一个问题,你需要一步一步地分析,一步一步地实现,所以说面向过程是具体的,流程化的. 打个比方,你家里来客人,你得请人家吃饭.第1步,你得去买菜,第2步,你得学做菜,第3步,你得洗菜,炒菜,煮饭. 所有的事情都得按照流程亲力亲为,这就是用面向过程的方式解决问题.
面向对象 面向对象是模型化的,已经提前把需要的功能和方法抽象成了一个类,需要什么功能直接使用就可以了,不必去一步一步的实现. 再打个比方,有客人来你家做客,你不会做饭,你在网上请个厨师到家,买菜,做菜全部交给他,这就是用面向对象的方式解决问题.
为什么要面向对象编程 ?
1. 抽象能力: 面向对象编程可以将现实世界中的各种对象抽象成程序中的类,通过对抽象的概念进行针对性设计 和实现,使得程序更加灵活、易于维护和扩展。
2. 封装性: 面向对象编程可以通过封装,将某些数据和方法隐藏起来,只向外部提供必要的接口,避免外部直接访问内部数据,保证了程序的安全性和稳定性。
3. 继承性: 面向对象编程允许通过继承的方式将已有的类的特性和行为传递给新的类,从而减少代码的重复,提高代码的复用性和可维护性。
4. 多态性:面向对象编程可以通过多态和接口的应用,使得不同的实现方式共享同一种实现接口,提高了代码的灵活性和可扩展性。
类与对象
现实世界中,事物与事物之间具有共同特征,例如:每个人都有姓名、身份证号、身高等状态,都有吃、跑、跳等行为。将这些共同的状态和行为提取出来,形成了一个模板,称为类。
状态在程序中对应属性。属性通常用变量来表示,行为在程序中对应方法,用方法来描述行为动作。
类 = 属性 + 方法。
对象是实际存在的个体,类可以实例化对象
对象又称为实例,通过类可以创造多个对象, 例如通过“人类”可以创造出“我“对象和“你"对象
“你”和“我”由于是通过人类造出来的,所以这两个都有name属性,但是值是不同的。因此这种属性被称为实例变量
类的创建
public class Student {
//三者都是实例变量,也是成员变量,如果没有赋值,则有默认值
//姓名
String name;
//年龄
int age;
//性别
boolean gender;
}
对象的创建、赋值
public class StudentText01 {
public static void main(String[] arges) {
//通过Student类创建对象
Student s1 = new Student();
//这里没有给对象中的实例赋值,所以会输出默认值
System.out.println("姓名:" + s1.name); //null
System.out.println("年龄:" + s1.age); //0
System.out.println("姓别:" + s1.gender); //false
//给对象s1赋值
s1.name = "张三";
s1.age = 18;
s1.gender = true; //男
System.out.println("姓名:" + s1.name);
System.out.println("年龄:" + s1.age);
System.out.println("姓别:" + s1.gender);
//创建第二个对象
Student s2 = new Student();
s2.name = "李四";
s2.age = 19;
s2.gender = false;
System.out.println("姓名:" + s2.name);
System.out.println("年龄:" + s2.age);
System.out.println("姓别:" + s2.gender);
}
}
实例方法
创建实例方法study
public void study(){
System.out.println("正在学习ing...");
}
通过对象调用实例方法
s1.study();
s2.study();
(这里不能通过类名调用,因为这是实例(对象)方法,包括name,age,gender)
空指针问题
public class StudentText01 {
public static void main(String[] arges) {
//通过Student类创建对象
Student s1 = new Student();
//给对象s1赋值
s1.name = "张三";
s1.age = 18;
s1.gender = true; //男
//创建第二个对象
Student s2 = new Student();
s2.name = "李四";
s2.age = 19;
s2.gender = false;
s1 = null; //将s1对象置空,此时s1的实例变量还在内存中,但是已经没有引用指向它了,所以可以将其回收掉
//s1.study(); //报错,因为s1已经置空,所以不能调用study方法
s2.study();
}
}
报错
this关键字
this在方法栈帧局部变量表中的0号槽位,指的是当前对象的地, this本质上是一个引用,该引用保存当前对象的内存地址。
当我们想要在方法种添加实例变量时,我们可以使用this.变量名
public void study(){
System.out.println(this.name + "正在学习ing...");
}
这里的this是可以省略的,用于区分局部变量和实例变量时不能省略,this不能出现在静态方法中。(局部变量与实例变量名字相同时)
通过“this.”可以访问实例变量,可以调用实例方法。
this也可以被直接输出,输出的内容是当前对象的地址。
s1.Putthis();
public void Putthis(){
System.out.println(this);
}
(这里this还有其他用法,放在构造方法之后)
封装
我们先看如下代码
package oop;
public class StudentText01 {
public static void main(String[] arges) {
Student s1 = new Student();
s1.name = "张三";
s1.age = 18;
s1.gender = true;
//这里我们可以对对象的age属性进行随意修改,这里输入的是一个负数,这不合理,也不安全,但程序还是做了
s1.age = -18;
System.out.println("age:"+s1.age);
}
}
为了保证安全合法,我们可以使用封装来实现。
第一 用private修饰类中的实例变量
private int age;
这里我们会看到报错这是因为我们使用了private,外部程序就不能直接访问(读和改)该实例变量,只能在该类下访问。
那当我们有需要时,我们怎样才能读取和修改呢?
第二 对外提供访问接口
我们可以在类中放入一个方法,通过这个方法我们可以实现数据的读和改。
public int getAge(){
return age;//省略this
}
public void setAge(int Age){
if(Age<0){
System.out.println("年龄错误");
return;
}
age = Age;
}
我们调用这两个方法可以实现age的修改
public class StudentText01 {
public static void main(String[] arges) {
Student s1 = new Student();
s1.name = "张三";
s1.gender = true;
s1.setAge(20);
System.out.println("age:" + s1.getAge());
}
}
当我们输入信息不合规时,setAge会返回错误
s1.setAge(-15);
System.out.println("age:" + s1.getAge());
方法封装之后我们也不能通过对象来调用,可以通过类名调用。
构造方法Constructor(构造器)
构造方法
构造方法的执行分为两个阶段:对象的创建和对象的初始化。这两个阶段不能颠倒,也不可分割。
在Java中,当我们使用关键字new时,就会在堆内存中创建一个新的对象(所以我们才能在构造方法中通过this为对象初始化),虽然对象已经被创建出来了,但还没有被初始化。初始化则在执行构造方法体时进行的。
构造方法名只能是类名。
构造方法的定义
[修饰符列表] 构造方法名 (形参) {}
构造方法的调用
new 构造方法名(实参);
关于无参数构造方法:如果一个类没有显示的定义任何构造方法,系统会默认提供一个无参数构造方法,也被称为缺省构造器。下面就是一个缺省构造器,也是系统自己创建的。
public Student(){
}
当我们在类里定义了构造方法,则缺省构造器将不存在。
public Student(String name,int age,boolean gender){
this.name = name; //this代表当前对象,这里因为有参数也叫name,所以用this.name
this.age = age;
this.gender = gender;
}
public static void main(String[] arges) {
Student s1 = new Student("张三",20,true); //这里可以直接为私有属性赋值
Student s2 = new Student();
System.out.println("name:" + s1.name);
System.out.println("age:" + s1.getAge());
System.out.println("gender:" + (s1.gender ? "男" : "女"));
}
这里就会报错
构造方法支持重载机制。所以我们可以定义传入各种参数的构造方法。
public Student(String name,int age,boolean gender){
this.name = name; //this代表当前对象,这里因为有参数也叫name,所以用this.name
this.age = age;
this.gender = gender;
}
public Student(String name){
this.name = name;
}
public Student(String name,int age){
this.name = name;
this.age = age;
}
public Student(String name,boolean gender){
this.name = name;
this.gender = gender;
}
public Student(){
}
public static void main(String[] arges) {
Student s1 = new Student("张三",20,true); //这里可以直接为私有属性赋值
Student s2 = new Student("李四");
Student s3 = new Student("王五",20);
Student s4 = new Student();
System.out.println("name:" + s1.name);
System.out.println("age:" + s1.getAge());
System.out.println("gender:" + (s1.gender ? "男" : "女"));
System.out.println("name:" + s2.name);
System.out.println("age:" + s2.getAge());
System.out.println("gender:" + (s2.gender ? "男" : "女"));
System.out.println("name:" + s3.name);
System.out.println("age:" + s3.getAge());
System.out.println("gender:" + (s3.gender ? "男" : "女"));
System.out.println("name:" + s4.name);
System.out.println("age:" + s4.getAge());
System.out.println("gender:" + (s4.gender ? "男" : "女"));
}
构造代码块
关于构造代码块。对象的创建和初始化过程:
① new的时候在堆内存中开辟空间,给所有属性赋默认值
② 执行构造代码块进行初始化
③ 执行构造方法体进行初始化
④ 构造方法执行结束,对象初始化完毕。
语法:
{ }
在类里加入如下代码
{
System.out.println("构造代码块执行");
}
上面的代码就会这样输出
this
“this(实参)”语法:
①只能出现在构造方法的第一行。
②通过当前构造方法去调用本类中其他的构造方法。
③作用是:代码复用。
下面我们来实现:当创建一个学生对象时name,age默认值分别为无名氏,-1
public class Student01 {
String name;
int age;
public Student01(String name, int age) {
this.name = name;
this.age = age;
}
public Student01(){
this("无名氏",-1);
}
}
public static void main(String[] arges) {
Student01 s4 = new Student01();
System.out.println("name:" + s4.name);
System.out.println("age:" + s4.age);
}
static关键字
静态变量,静态方法
String name;
private int age;
boolean gender;
String nationality = "中国";
当我们为学生定义国籍时,我们可以用上面这段代码,将nationality定义为实例变量 ,但这样会占用很多内存空间(每一个对象都会在堆里分配内存)。
当我们的实例变量都一样时,建议定义为静态变量,这样只会占用一块空间。
static String nationality = "中国";
System.out.println("nationailty:" + Student.nationality);
static是一个关键字,翻译为:静态的。
static修饰的变量叫做静态变量,static修饰的方法叫做静态方法。
当所有对象的某个属性的值是相同的,建议将该属性定义为静态变量,来节省内存的使用。
静态变量在类加载时初始化,存储在堆中,类加载时初始化。
所有静态变量和静态方法,统一使用“类名.”调用。(也可以使用“引用.”来调用,不建议) 使用“引用.”访问静态相关的,即使引用为null,也不会出现空指针异常。(不需要对象参与
Student s2 = new Student("李四");
System.out.println("nationailty:" + Student.nationality);
System.out.println("nationailty:" + s2.nationality);
s2 = null;
System.out.println("nationailty:" + s2.nationality);
静态方法中不能使用this关键字。因此无法直接访问实例变量和调用实例方法。
静态代码块在类加载时执行,一个类中可以编写多个静态代码块,遵循自上而下的顺序依次执行。
静态代码块代表了类加载时刻,如果你有代码需要在此时刻执行,可以将该代码放到静态代码块中。
静态代码块
类加载时执行,只执行一次
String a = "hello";
static String b = "hello";
//静态代码块在在类加载时初始化,会按编写顺序执行
static {
//System.out.println(a); //这里不能输出hello,因为a是静态变量,在静态上下文中无法直接访问实例相关的数据。
System.out.println("静态代码块1执行");
}
static {
System.out.println(b);
System.out.println("静态代码块2执行");
}
static {
//System.out.println(c); //不能输出hello,因为c是static变量,在类加载时从上往下加载,此时c还没有初始化。
System.out.println("静态代码块3执行");
}
static String c = "hello";
单例模式Singleton(初级)
饿汉式
饿汉式单例模式:类加载时对象就创建好了。
public class Student {
//第三步:定义一个静态变量,在类加载的时候,初始化静态变量。(只初始化一次)
private static Student a = new Student();
//第一步:创造方法私有化。
private Student() {}
/*
这段代码的主要功能是通过将 Student 类的构造方法声明为私有,来限制该类的外部实例化。
这样做的目的是为了防止其他类通过该类创建对象,从而避免了不必要的资源浪费。
*/
//第二步:对外提供一个静态的方法,用这个方法获取单个实例。
public static Student get() {
return a;
}
}
public class StudentText01 {
public static void main(String[] arges) {
//Student s1 = new Student();
Student s2 = Student.get();
Student s3 = Student.get();
System.out.println(s3 == s2); //比较两个对象是否相同
}
}
懒汉式
懒汉式单例模式:。
public class Student {
//第三步:定义一个静态变量,在类加载的时候,初始化静态变量。(只初始化一次)
private static Student a = null;
//第一步:创造方法私有化。
private Student() {}
//第二步:对外提供一个静态的方法,用这个方法获取单个实例。
public static Student get() {
if(a == null)
return a = new Student();
else
return a;
}
}
继承----面向对象三大特征之一
继承的作用:
基本作用:代码复用
下面这个代码定义了一个人类,我接下来想定义一个学生类,而学生类里也会有这些方法和人类里的一样,如果这样就很不好,那么我们可以让学生类继承人类
继承在java中的实现
[修饰符列表] class 类名 extends 父类名
extends翻译为扩展。表示子类继承父类后,子类是对父类的扩展 继承相关的术语:当B类继承A类,A类称为:父类、superclass,B类称为:子类、subclass
public class Person {
String name;
int age;
boolean gender;
public Person(String name, int age, boolean gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public Person(){}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setGender(boolean gender) {
this.gender = gender;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public boolean getGender() {
return gender;
}
Student类里就可以省略人类里有的,Person是父类,Student是子类
public class Student extends Person{
double grade;
public double getGrade() {
return grade;
}
public void setGrade(double grade) {
this.grade = grade;
}
public void Study(){
System.out.println(name + " is studying");
}
}
public static void main(String[] arges) {
Student s1 = new Student();
s1.setName("John");
s1.setGrade(99.9);
s1.setAge(18);
s1.setGender(true);
System.out.println("Name: " + s1.getName());
System.out.println("Age: " + s1.getAge());
System.out.println("Gender: " + s1.getGender());
System.out.println("Grade: " + s1.getGrade());
}
重要作用:有了继承,才有了占法覆盖和多态
Java只支持单继承,一个类只能直接继承一个类,Java不支持多继承,但支持多重继承(多层继承)。子类继承父类后,除私有的不支持继承、构造方法不支持继承。其它的全部会继承。
public class Cc {
public static void main (String[] args){
C c = new C();
c.a();
c.b();
c.c();
}
}
class A {
public void a() {
System.out.println("Hello from A");
}
}
class B extends A {
public void b() {
System.out.println("Hello from B");
}
}
class C extends B {
public void c() {
System.out.println("Hello from C");
}
}
一个类没有显示继承任何类时,默认继承java.lang.object类。
方法覆盖(override)/方法重写(overwrite)
什么情况下使用方法覆盖:
当从父类中继承过来的方法无法满足当前子类的业务需求时
发生方法覆盖的条件:
具有继承关系的父子类之间
相同的返回值类型,相同的方法名,相同的形式参数列表
现在定义了一个动物类的,有吃和移动,如果定义了一个鱼类,那么running就不合适了
public class Animal {
public void eat() {
System.out.println("Animal is eating");
}
public void move() {
System.out.println("Animal is running");
}
}
我们可以在鱼类里使用方法重载
public class Fish extends Animal {
public void move() {
System.out.println("Fish swimming");
}
}
@Override注解标注的方法会在编译阶段检查该方法是否重写了父类的方法。
@Override
public void Move() {
System.out.println("Fish swimming");
}
因为Move不是父类方法,所以报错 。
当我们在Animal类里创建了下面这个方法,当参数列表不一样时,会报错
public Object re(int a,String b){
return a+b;
}
@Override
public Object re(long a,String b){
return a+b+b;
}
但当我们去掉@Override时,发生了方法重载,不报错。
当我们使用引用类型返回值时,我们可以使用引用类型的子类型来做返回值
返回值类型可以是父类方法返回值类型的子类
当我们覆盖时使用Object类下的String时不会报错,但反过来就会报错
@Override
public String re(int a,String b){
return a+b+b;
}
私有方法、构造方法不能继承,所以不能覆盖。
访问权限不能变低,可以变高。
抛出异常不能变多,可以变少。
静态方法不存在方法覆盖,方法覆盖针对的是实例方法。
方法覆盖针对的是实例方法,和实例变量无关。(到多态是解释)
软件开发原则
旨在引导软件行业的从业者在代码设计和开发过程中,道循一些基本原则,以达到高质量、易维护、易扩展、安全性强等目标。软件开发原则与具体的编程语言无关的,属于软件设计方面的知识。
这里主要了解开闭原则,为多态服务
开闭原则(0pen-Closed principle,OCP)
一个软件实体应该对扩展开放,对修改关闭。即在不修改原有代码的基础上,通过添加新的代码来扩展功能。(最基本的原则,其它原则都是为这个原则服务的。)
单一职责原则
一个类只负责单一的职责,也就是一个类只有一个引起它变化的原因。
里氏替换原则
子类对象可以替换其基类对象出现的任何地方,并且保证原有程序的正确性。
接口隔离原则
客户端不应该依赖它不需要的接口。
依赖倒置原则
高层模块不应该依赖底层模块,它们都应该依赖于抽象接口。换言之,面向接口编程。
迪米特法则
一个对象应该对其它对象保持最少的了解。即一个类应该对自己需要耦合或调用的类知道得最少
合成复用原则
尽量使用对象组合和聚合,而不是继承来达到复用的目的。组合和聚合可以在获取外部对象的方法中被调用,是一种运行时关联,而继承则是一种编译时关联。
多态
基础语法
类型转换
基本数据类型
小容量转大容量----自动类型转换
大容量转小容量----强制类型转换
引用数据类型
前提:无论向上还是向下,两种类型必须有继承关系才可以
向上转型(upcasting) 子 ---->父
public class AnimalText {
public static void main(String[] args) {
Animal a = new Animal();
Cat c = new Cat();
Fish f = new Fish();
Animal a2 = new Cat();
Animal a3 = new Fish();
a2.move();
a3.move();
}
}
java程序包括两个重要的阶段:
第一阶段:编译阶段
在编译的时候,编译器只知道a2的类型是Animal类型因此在编译的时候就会去Animal类中找move()方法
找到之后,绑定上去,此时发生静态绑定。能够绑定成功,表示编译通过。
第二阶段:运行阶段
在运行的时候,堆内存中真实的java对象是Cat类型所以move()的行为一定是Cat对象发生的
因此运行的时候就会自动调用Cat对象的move()方法
这种绑定称为运行期绑定/动态绑定。
核心:父类型引用指向子类型对象。
那当我们在Cat类里定义了catchMouse 方法(Animal类里没有),此时用a2调用就会报错
public void catchMouse (){ System.out.println("Cat is chating with mouse"); }
编译错误
向下转型(downcasting) 父---->子
那a2就不能调用catchMouse方法了吗?我们可以使用向下转型
Cat c2 = (Cat) a2; c2.catchMouse();
所以,当我们调用的方法是子类特有的方法时,我们要向下转型
但是要注意两类型要有继承关系,否则编译无法通过
Fish f2 = new Fish(); Cat c3 = (Cat) f2;
下面这个代码因为Fish类继承了Animal类,编译能够通过
Animal a4 = new Cat(); Fish f3 = (Fish) a4;
但是运行时会报错
因为运行时堆中a4类型是Cat类
为了避免ClassCastException错误,我们可以使用instanceof来判断
instanceof
语法: 引用a instanceof A类型
instanceof运算符的结果一定是boolean类型(true/false)
true:引用a指向的对象是A类型
false:引用a指向的对象不是A类型
Animal a4 = new Cat();
if(a4 instanceof Fish) {
Fish f3 = (Fish) a4;
}
else System.out.println("a4 is not a Fish");
多态解决的问题
问题引入
现有一客户养了一只猫,你为他提供了如下服务
public class Cat {
// 喂猫
public void eat() {
System.out.println("Cat is eating");
}
public class Master {
public void Feed(Cat c) {
c.eat();
}
public class Text {
public static void main(String[] args){
Master m = new Master();
Cat c = new Cat();
m.Feed(c);
}
}
但后来客户养了一条狗,你若想要满足客户的需求,你可以写如下代码:
public class Dog {
//喂狗
public void eat() {
System.out.println("Dog is eating");
}
}
public class Text {
public static void main(String[] args){
Master m = new Master();
Cat c = new Cat();
Dog d = new Dog();
m.Feed(c);//m.Feed(d);
}
}
但如此一来就修改了Master里的代码,不符合OCP原则
那怎么办呢?
这里我们就要用到多态了
解决问题
为解决客户想要养多种动物的需求,我们可以定义一个父类宠物,子类为各种宠物
public class Cat extends Pet{
// 喂猫
public void eat() {
System.out.println("猫在吃鱼");
}
}
public class Dog extends Pet{
//喂狗
public void eat() {
System.out.println("狗在啃骨头");
}
}
public class Master {
public void Feed(Pet p) { //这里用了多态
p.eat();
}
}
public class Pet {
public void eat() {}
}
public class Text {
public static void main(String[] args){
Master m = new Master();
Cat c = new Cat();
Dog d = new Dog();
m.Feed(c); //父类p引用指向Cat类型对象
m.Feed(d); //父类p引用指向Dog类型对象
}
}
这样就算顾客再养其他宠物,我们也可以在保证不改变Master的情况下实现
方法重载的补充
静态方法不存在方法覆盖,方法覆盖针对的是实例方法。因为覆盖和多态联系起来才有意义
public class A {
public static void S(){
System.out.println("A");
}
public class B extends A {
public static void S(){
System.out.println("B");
}
}
public class S {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.S(); //A
b.S(); //B
A m = new B();
m.S(); //A
}
}
看似实现了静态方法的覆盖,但其实没有,因为
静态方法属于类级别(而非对象级别),调用时不需要创建对象实例,直接通过类名即可调用。在Java中,静态方法不支持多态性(无法被重写),因为它们的绑定发生在编译时(静态绑定)。
多态性的实现依赖于对象继承和方法重写(动态绑定),需要子类对象通过父类引用调用被重写的方法,其具体行为在运行时根据实际对象类型决定。这属于运行时多态(动态多态)。
方法覆盖针对的是实例方法,和实例变量无关。
public class S {
public static void main(String[] args) {
a a1 = new b();
System.out.println(a1.name); //a
}
}
class a{
String name = "a";
}
class b extends a{
String name = "b";
}
抽象类和抽象方法
在上面顾客养宠物的例子中,我们父类Pet中的eat方法是这样设计的:
public class Pet {
public void eat() {}
}
我们这里使用抽象类:
public abstract class Pet { //当定义了抽象方法,就必须定义为抽象类
// 当定义了抽象方法,在子类中必须重写该方法
public abstract void eat();
}
关键字public、abstract没有顺序要求
当定义了抽象方法,就必须定义为抽象类,当我们定义了继承了一个抽象类的类,那么这个类也必须是abstract修饰的
那我们为什么要使用抽象方法呢?
不同类型的动物,吃的方式、食物肯定是不同的。
因此,动物具体是怎么吃的,eat中是不确定,无法实现的。
针对这种方法既然不确定具体的实现是什么,那么就不应该给实现
注意:在java中,只要一个方法带着大括号,不管大括号中有什么,只要有大括号就表示一种实现。
因此像这种无法确定实现的方法,建议定义为抽象方法。
什么时候将类定义为抽象类
类定义为抽象类。这样在抽象类中只提公共代码代码
如果类中有些方法无法实现或者没有意义,可以将方法定义为抽象方法,强行交给子类去做。
抽象类的定义
abstract class 类名{}
抽象类有构造方法,但无法实例化(生成对象),是给子类带调用的(super())。
抽象方法的定义
abstract 返回值类型 方法名(形参)
抽象类中不一定有抽象方法,但如果有抽象方法那么类要求必须是抽象类。
一个非抽象的类继承抽象类,要求必须将抽象方法进行实现/重写。
abstract关键字不能和private(不能被继承),final(不能被重写),static(不能方法覆盖)关键字共存。
super关键字
我们可以super关键字和this关键字对比来学习。
this代表的是当前对象,super代表的是当前对象中的父类型特征(从父类继承下来的)。
public class Person { //父类
String name;
int age;
boolean gender;
public Person(String name, int age, boolean gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public Person(){}
}
public class Student extends Person {
double grade;
public void printInfo(){
System.out.println("Name: " + super.name);
System.out.println("Age: " + super.age);
System.out.println("Gender: " + super.gender);
System.out.println("Grade: " + super.grade);
}
}
这里的grade会报错,因为grade不是父类继承下来的,要使用this(父类的也可以用this)
super不能使用在静态上下文中。
super
用于访问 父类的实例成员(实例方法、实例变量),而 静态上下文属于类级别,与具体实例无关。静态方法/代码块在类加载时初始化,此时可能还没有任何实例被创建。
- 静态成员属于类,而非实例,
super
的语义是“当前实例的父类引用”,二者逻辑冲突。- 如果允许
super
在静态上下文中使用,会导致歧义(静态上下文没有this
,自然也没有super
)。
“super.”大部分情况下是可以省略的。
不能省略:
当父类和子类中定义了相同的属性(实例变量)或者相同方法(实例方法)时,如果需要在子类中访问父类的属性或方法时,super.不能省略。
public class StudentText01 {
public static void main(String[] arges) {
Student s1 = new Student("John", 18, true, 99.9);
s1.printInfo();
}
}
public class Person {
String name;
int age;
boolean gender;
public Person(String name, int age, boolean gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
}
public class Student extends Person {
public Student(String name, int age, boolean gender, double grade) {
//super(); //调用父类无参构造方法,赋值默认值
this.name = name;
this.age = age;
this.gender = gender;
this.grade = grade;
}
String name;
double grade;
public void printInfo(){
System.out.println("Name: " + super.name); //John
System.out.println("Age: " + super.age);
System.out.println("Gender: " + super.gender);
System.out.println("Grade: " + this.grade);
System.out.println("Name: " + this.name); //null
System.out.println("Age: " + this.age);
System.out.println("Gender: " + this.gender);
System.out.println("Grade: " + this.grade);
}
}
super.name是在public Student(String name, int age, boolean gender, double grade)执行初被super()执行后(调用父类无参构造方法)赋值null,这个name是父类的,下面的this.name才会给子类的name'赋值Jone。
age,gender也被赋值过默认值(super();),而后被this.name,this.gender赋值修改
再看看实例方法的例子
public void doSome(){
System.out.println("Person is doing something.");
}
public void doSome(){
System.out.println("I am a student");
//this.doSome(); //调用当前doSome,一直递归
super.doSome(); //调用父类的doSome //省略super也会是this调用
System.out.println("Studying cone to an end.");
}
this(引用)可以单独输出(地址),super(不是地址,只是代表了父类型特征)不能单独输出。
super (实参):通过子类的构造方法调用父类的构造方法,目的是为了完成父类型特征的初始化。
因为我们在Person父类中已经有了对那,name,age,gender的初始化,所以我们可以调用父类的构造方法来实现代码复用。
public Student(String name, int age, boolean gender, double grade) { // this.name = name; // this.age = age; // this.gender = gender; super(name, age, gender); this.grade = grade; }
当一个构造方法第一行没有显示的调用“super(实参);”,也没有显示的调用“this(实参)”,系统会自动调用super()。因此一个类中的无参数构造方法建议显示的定义出来。
当父类没有无参数构造方法(有其他构造方法)时,子类中的无参数构造会报错
public Student() {
//super(); //调用父类的构造方法,不写的话默认有 //赋值默认值
}
public Student(String name, int age, boolean gender, double grade) {
//System.out.println("error"); //super(); 必须写在第一行
super(name, age, gender);
this.grade = grade;
}
所以super()主要作用是通过子类构造方法调用父类构造方法来给继承来的赋值
public class Cc {
public static void main (String[] args){
C c = new C();
}
}
class A {
public A() {
System.out.println("Hello from A"); //1
}
}
class B extends A {
public B() {
System.out.println("Hello from B"); //2
}
}
class C extends B {
public C() {
System.out.println("Hello from C"); //3
}
}
final关键字
final修饰的类不能被继承
final修饰的方法不能被覆盖
final修饰的变量,赋值后不能重新赋值
final修饰的实例变量必须在对象初始化时手动赋值
final修饰的实例变量必须在构造方法执行之前手动赋值,不能用默认值,一般和static联合使用:称为常量 public static final 常量名 = 值;
final修饰的引用,一旦指向某个对象后,不能再指向其它对象。但指向的对象内部的数据是可以修改的。
接口(interface)
基础语法
在Java中,接口(interface)是一种抽象的数据类型,它定义了一组抽象的方法,但没有具体的实现。接口和类一样,也是一种引用数据类型。
接口在Java中的作用包括但不限于以下几点:
-
实现多重继承:Java中的类是单一继承的,但是一个类可以实现多个接口,从而实现了多重继承的效果。
-
定义规范:接口定义了一组方法的规范,类实现接口时必须提供这些方法的具体实现,确保了代码的统一性和一致性。
-
降低耦合性:接口定义了类与类之间的契约,类通过实现接口来与其他类交互,降低了类之间的耦合度。
接口和类的区别主要有以下几点:
-
接口中的方法都是抽象的,没有具体的实现代码;类可以包含实现了的方法。
-
类只能单继承,但可以实现多个接口;接口支持多继承。
-
接口不能含有实例字段,而类可以含有实例字段。
-
类可以拥有构造函数,而接口不能有构造函数。
接口的定义:
[修饰符列表]interface 接口名{}
public interface I1 {}
抽象类是半抽象的,接口是完全抽象的。接口没有构造方法,也无法实例化对象。
JDK8之前,接口中只能定义常量和抽象方法。
接口中的常量的static、final、抽象方法abstract可以省略(默认有)。接口中所有的方法和变量都是public修饰的。
接口和接口之间可以多继承。接口的方法不能有方法体。
public interface I1 { void i1(); } interface I2{ void i2(); } interface I3 extends I1, I2{ void i3(); } interface I4{ void i4(); }
类和接口的关系叫做实现(这里的实现也可以等同看做继承)。使用implements关键字进行接口的实现。
一个非抽象的类实现接口必须将接口中所有的抽象方法全部实现。
一个类可以实现多个接口。
语法是:class 类 implements 接口A,接口B{}
class A implements I3,I4 { @Override public void i1() { System.out.println("I1"); } @Override public void i2() { System.out.println("I2"); } @Override public void i3() { System.out.println("I3"); } @Override public void i4() { System.out.println("I4"); } }
public class InterfanceText { public static void main(String[] args) { A ii = new A(); ii.i1(); ii.i2(); ii.i3(); ii.i4(); } }
多态下
public class InterfanceText { public static void main(String[] args) { I3 ii = new A(); //父类(I3)引用(ii)指向子类(A)对象 ii.i1(); ii.i2(); ii.i3(); ii.i4(); //错误 } }
Java8之后,接口中允许出现默认方法和静态方法(JDK8新特性)
① 引入默认方式是为了解决接口演变问题:接口可以定义抽象方法,但是不能实现这些方法。所有实现接口的类都必须实现这些抽象方法,这会导致接口升级的问题:当我们向接口添加或删除一个抽象方法时,这会破坏该接口的所有实现,并且所有该接口的用户都必须修改其代码才能适应更改。这就是所谓的“接口演变"问题。
default默认方法要有方法体
default void defaultMethod(){ System.out.println("default默认方法体"); }
ii.defaultMethod();
class A implements I3,I4 { @Override public void defaultMethod(){ System.out.println("A类重写了默认方法"); } }
② 引入的静态方法只能使用本接口名来访问,无法使用实现类的类名访问。
注意:java中规定,在JDK8之后,接口中可以定义静态方法,但是这个静态方法,只能通过“该接口名”去调用的,别的都无法调用
在JDK8之后引入接口可以定义静态方法,实际上想表达一个意思:接口也可以作为工具来使用了
JDK9之后允许接口中定义私有的实例方法(为默认方法服务的)和私有的静态方法(为静态方法服务的)
public interface I1 {
void i1();
default void defaultMethod(){
System.out.println("default默认方法体");
privateDefaultMethod();
}
static void staticMethod(){
System.out.println("static静态方法");
privateStaticMethod();
}
private static void privateStaticMethod(){
System.out.println("private静态方法");
}
private void privateDefaultMethod(){
System.out.println("private默认方法");
}
}
ii.defaultMethod();
I1.staticMethod();
(这里要记得把之前在类里面重写的default方法注释掉)
所有的接口隐式的继承Object。因此接口也可以调用Object类的相关方法。
当一个类要继承和实现接口时,先继承后实现,单继承多实现
public class YingWu extends Animal implements Fly,Speckable
接口与抽象类如何选择
①抽象类和接口虽然在代码角度都能达到同样的效果,但适用场景不同:
①抽象类主要适用于公共代码的提取。当多个类中有共同的属性和方法时,为了达到代码的复用,建议为这几个类提取出来一个父类,在该
父类中编写公共的代码。如果有一些方法无法在该类中实现,可以延迟到子类中实现。这样的类就应该使用抽象类。
②接口主要用于功能的扩展。例如有很多类,一些类需要这个方法,另外一些类不需要这个方法时,可以将该方法定义到接口中。需要这个
方法的类就去实现这个接口,不需要这个方法的就可以不实现这个接口。接口主要规定的是行为。
UML
UML(UnifiedModeling Language,统一建模语言)是一种用于面向对象软件开发的图形化的建模语言。它由Grady Booch、James Rumbaugh和lvar Jacobson等三位著名的软件工程师所开发,并于1997年正式发布。UML提供了一套通用的图形化符号和规范,帮助开发人员以图形化的形式表达软件设计和编写的所有关键方面,从而更好地展示软件系统的设计和实现过程。
UML是一种图形化的语言,类似于现实生活中建筑工程师画的建筑图纸,图纸上有特定的符号代表特殊的含义。
UML不是专门为java语言准备的。只要是面向对象的编程语言,开发前的设计,都需要画UML图进行系统设计。(设计模式、软件开发七大原则等同样也不是只为java语言准备的。
UML图包括:
类图(ClassDiagram》:描述软件系统中的类、接口、关系和其属性等;
用例图(UseCaseDiagram):描述系统的功能需求和用户与系统之间的关系;
序列图(SequenceDiagram》:描述对象之间的交互、消息传递和时序约束等;
状态图(StatechartDiagram》:描述类或对象的生命周期以及状态之间的转换;
对象图(ObjectDiagram):表示特定时间的系统状态,并显示其包含的对象及其属性;
协作图(CollaborationDiagram》:描述对象之间的协作,表示对象之间相互合作来完成任务的关系;
活动图(ActivityDiagram》:描述系统的动态行为和流程,包括控制流和对象流;
部署图(DeploymentDiagram):描述软件或系统在不同物理设备上部署的情况,包括计算机、网络、中间件、应用程序等。
常见的UML建模工具有:StarUML,Rational Rose等。
类之间的关系
1. 泛化关系(is a)
2. 实现关系(is like a)
3. 关联关系(has a)
4. 聚合关系
聚合关系指的是一个类包含、合成或者拥有另一个类的实例,而这个实例是可以独立存在的。聚合关系是一种弱关联关系,表示整体与部分之间的关系。例如一个教室有多个学生
5. 组合关系(Composition)
组合关系是聚合关系的一种特殊情况,表示整体与部分之间的关系更加强烈。组合关系指的是一个类包含、合成或者拥有另一个类的实例,而这个实例只能同时存在于一个整体对象中。如果整体对象被销毁,那么部分对象也会被销毁。例如一个人对应四个肢体。
6. 依赖关系(Dependency)
依赖关系是一种临时性的关系,当一个类使用另一个类的功能时,就会产生依赖关系。如果一个类的改变会影响到另一个类的功能,那么这两个类之间就存在依赖关系。依赖关系是一种较弱的系,可以存在多个依赖于同一个类的对象。例如A类中使用了B类,但是B类作为A类的方法参数或者局部变量等。
(这里可以用StarUML来画)
控制访问权限
private:私有的,只能在本类中访问。
缺省(什么也不写):默认的,同一个包下可以访问。
protected:受保护的,子类中可以访问。(受保护的通常就是给子孙用的。)
public:公共的,在任何位置都可以访问。
类中的属性和方法访问权限共有四种:private、缺省、protected和public。
类的访问权限只有两种:public和缺省。
访问权限控制符不能修饰局部变量。
设计原则建议
- 最小化开放权限:优先使用
private
或default
,减少对外暴露。 - 继承与扩展:
protected
适用于需要子类扩展但无需公开的场景。 - 模块化设计:通过
default
权限实现包内高内聚、包间低耦合。
(持续更新学习中......)