面向对象

一、面向对象的基本概念

基本概念

面向过程:

所谓面向过程:将要实现的功能描述为从一个开始到结束按部就班的连续步骤;依次逐步完成这些步骤,如果某一步难度较大,又可以将该步骤再次细化为若干子步骤,以此类推,一直到结束得到想要的结果;程序的主体是函数,一个函数就是封装起来的模块,可以实现一定的功能(某一动作),各个子步骤往往就是通过各个函数来完成的,从而实现代码的重用

面向对象:
类,实体,属性,动作(方法)
所谓面向对象就是:在编程的时候尽可能去模拟真实的现实世界,按照现实世界中的逻辑处理问题,分析问题中参与的有哪些实体,这些实体应该有什么属性和方法,我们如何通过这些实体的属性和方法去解决问题。

实体是动作的支配者,没有实体就肯定没有动作发生。任何功能的实现都是依赖于一个具体的实体的"动作,操作,行动"。

面向对象可以看作是一个又一个的实体在发挥其各自“能力“并在内部进行协调有序的调用过程。分析过程为:

  • 分析有哪些动作是由哪些实体发出 的
  • 定义这些实体,为其增加相应的属性和功能
  • 让实体去执行相应的功能和动作

类与对象的关系

用Java语言对现实生活中的事物进行描述。一般关注两个方面,属性行为(动作,功能,函数)

​ 只要明确该事物的属性和行为并定义在类中即可。(描述用)

对象

就是该类事物实实在在存在的个体。用new关键字来创建该类事物的对象(具体怎么描述的,是个实例)

成员

  • 定义类就是定义类中的成员
  • 成员变量:属性;成员函数:行为
  • 成员变量与局部变量的区别
    • 成员变量在类中,整个类都可以访问
      局部变量定义在函数语句中,局部代码块,只在所属区域有效
    • 成员变量存在堆内存中
      局部变量在栈内存中
    • 成员变量随着对象创建而存在,随着对象的消失而消失
      局部变量随着所属区域的执行而存在,随着所属区域的结束而释放
    • 成员变量都有默认初始化值,而局部变量没有
      注意:类中怎么没有定义主函数?

​ 主函数是为了保证所在类的独立运行,是程序的入口,被jvm调用;主函数的存在,仅为该类是否需要独立运行,如果不需要,主函数不用定义的。

/*
描述小汽车
分析:
1,属性。
	轮胎数。
	颜色。 
2,行为。
	运行。
*/
class Car //没有定义主函数的原因是:专门描述某一事物,不需要独立运行,用到才运行
{
	int num;
	String color;
	void run()
	{
		System.out.println(num+"..."+color);
	}
}
class  CarDemo
{
	public static void main(String[] args) 
	{
		//在计算机中创建一个car的实例。通过new关键字。 
	Car c = new Car();// c就是一个类类型的引用变量,指向了该类的对象。
    c.num = 4;
    c.color = "red";
    c.run();//要使用对象中的内容可以通过  对象.成员 的形式来完成调用。

	Car c1 = new Car();
	Car c2 = new Car();
    show(c1);
    show(c2);
	new Car();//匿名对象。其实就是定义对象的简写格式。
	new Car().run();
	//汽车改装厂。
	//类类型参数
	public static void show(Car c)//类类型的变量一定指向对象。要不就是null。 
	{
		c.num = 3;
		c.color = "black";
		System.out.println(c.num+"..."+c.color);
	}
}

面向对象的特征

  • 封装
  • 继承
  • 多态

面向对象的注意事项

  • 在开发时,要找到数据所属的对象
  • 分析问题时,应寻找收集到的对象有哪些,而不是收集到的动作有哪些
  • 只关心对象,在这个待解决问题中有哪些对象;有对象用对象,没有对象制造对象再使用对象
  • 紧接着维护对象之间的关系

面向对象的内存体现以及基本类型参数传递图解

对象的内存图
对象的出现其实用来封装数据的。
每个对象都在封装自己的数据;数据也是一种对象,数组封装的是同一类数据,而对象封装的是一个事物的数组。
在这里插入图片描述
按值调用:表示方法接收的是调用者提供的值
按引用传递:表示方法接收的是调用者提供的变量地址

匿名对象

  • 当对象对方法进行一次调用的时候,就可以简化成匿名对象(上述代码有显示)
  • 匿名对象可以作为实际参数进行传递

二、封装

概念

隐藏对象的属性和实现细节,仅对外提供公共访问方式。

好处

  • 将变化隔离
  • 便于使用
  • 提高重用性
  • 提高安全性

封装原则

  • 将不需要对外提供的内容都隐藏起来
  • 将属性都隐藏起来,提供公共方法对其访问

关键字 private

一个权限修饰符,用于修饰成员,使其私有化,私有内容只有本类有效。
private避免了随意访问以及提高安全性

public class Person {

    private String name;
    private int gender;
    private int age;

    public String getName() {
        return name;
    }
    public void setName(int a){
          name=a
     }
    public String getGender() {
        return gender == 0 ? "man" : "woman";
    }
    public void work() {
        if (18 <= age && age <= 50) {
            System.out.println(name + " is working very hard!");
        } else {
            System.out.println(name + " can't work any more!");
        }
    }
}

变量一般有两个功能:1. 设置set () 2.获取get( )
对外提供方法的目的是为了对数据进行可控。

上面 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。

注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。

总结 :开发时,属性是用于存储数据的,直接被访问容易出现安全隐患,所以类中的属性通常被私有化,并对外提供公共访问方法

使用预定义类

构造函数

class Person
{
	private String name; //私有化,封装
	private int age;
   //定义一个Person类的构造函数。
	Person()//构造函数,而且是空参数的。
	{
		name = "baby";
		age = 1;
		System.out.println("person run")
	}
	Person(String n)//有参数的
	{
		name = n;
	}
	public void setName(String n) //提供对外访问方法
	{
		name = n;	
	}
	Person(String n,int a) //带参数的
	{
		name = n;
		age = a;		
	}
	public void speak()
	{
		System.out.println(name+":"+age);
	}

}
class ConsDemo
{
	public static void main(String[] args) 
	{
		Person p = new Person();//构造函数在创建对象时就运行了
//		p.speak();
		Person p1 = new Person("旺财");
		p1.setName("旺旺");

		p1.speak();

		Person p2 = new Person("小强",10);
		p2.speak();
	}
}

概述

构建创建对象时调用的函数,可以给对象以初始化,创建对象都必须要通过构造函数初始化。
什么时候定义构造函数:在描述事物时,该事物一存在就具备一些内容,这些内容定义在构造函数中。

特点

  • 函数名与类名相同
  • 不用定义返回值类型
  • 没有具体的返回值
  • 构造器可以有0个、1个或多个参数
  • 每个类可以有一个以上的构造器
  • 构造函数在对象一创建时就运行了,总是伴随着new操作一起调用
  • 构造函数可以对对象以初始化

构造函数与一般函数的区别

  • 构造函数:对象创建时就会调用与之对应的构造函数,对对象进行初始化
    一般函数:对象创建后,需要函数功能时才调用
  • 构造函数:对象创建时,会调用且只调用一次
    一般函数:对象创建后,可以被调用多次

默认构造函数

如果在构造器中没有显示地赋予初值,那么就会别自动地赋予默认值:数值为0、布尔值为false、对象引用为null。
但是这不是一个良好的编程习惯。

重载

构造函数可以有多个,用于对不同对象进行针对性的初始化。这种特征就叫做重载
如果多个方法有相同的名字、不同的参数,便产生重载。

内存图解

在这里插入图片描述
这里存在两个初始化,默认初始化name=null,age=0;以及构造函数初始化name=“小强”,age=10。
过程:

  1. 执行main方法时,在栈内存中开辟main方法的空间(压栈或进栈),然后在main方法的栈区分配一个变量p2
    2.在堆内存中 开辟一个实体空间,分配一个内存首地址(new),完成默认初始化
  2. 构造函数Person()进栈,name和age进行了构造函数初始化(这两个初始化同时)
  3. Person()函数出栈
  4. 初始化后赋给main()函数中的p2,实际是在在堆中的地址p2。
  5. Speak()进栈
  6. Speak()出栈

构造函数中的细节

  • 构造函数如果完成了set的功能,set是否需要?需要
  • 一般函数不能直接调用构造函数
  • 构造函数如果前面加了void就变成了一般函数
  • 构造函数中是有return语句的

三、关键字

this

  • 代表对象,就是所在函数所属对象的引用
  • this代表什么呢?
    哪个对象调用了this所在的函数,this就代表哪个对象,就是哪个对象的引用
  • this使用场景?
    当成员变量和局部变量重名的,用关键字this来区分
    在这里插入图片描述

    this的使用场景

    1. 当成员变量和局部变量重名,可以用关键字this来区分;在定义功能时,如果该功能内部使用到了调用该功能的对象,这时就用this来表示区分。
      一个类中,它的所有成员,只要想运行,都必须要有对象调用
    2. 用this来调用构造函数
    3. 用this调用构造函数,必须定义在构造函数的第一行。因为构造函数是用于初始化的,所以初始化动作一定要执行,否则编译失败。
      不要反复使用this()
class Person
{
	private String name;
	private int age;	
	Person()
	{		
		name = "baby";
		age = 1;
		System.out.println("person run");
	}	
	Person(String name)	
	{
		this();
		this.name = name;
	}
	Person(String name,int age)
	{
		this.name = name;
		this.age = age;		
	}
	public void speak()
	{
		System.out.println(this.name+":"+this.age);
	}
	//判断是否是同龄人
	public boolean compare(Person p)
	{
		return this.age==p.age;
	}
}
class ThisDemo 
{
	public static void main(String[] args) 
	{

		Person p1 = new Person("aa",30);
		Person p2 = new Person("zz",12);

		p2.compare(p1);
	}
}

static关键字

class Person
{
	private String name;//成员变量,实例变量
	private int age;
	static String country = "CN";//静态变量,类变量
	public Person(String name,int age)
	{
		this.name = name;
		this.age = age;
	}
	
	public void show()
	{
		System.out.println(Person.country+":"+this.name+":"+this.age);
	}

	public static void method() //静态方法
	{
		System.out.println(Person.country);
	}
}

class StaticDemo2 
{
	public static void main(String[] args) throws Exception
	{
		Thread.sleep(5000);
		Person.method();//直接被类名调用
		Person p = new Person("java",20);
		p.show();
	}
}
  • static是个修饰符,用于修饰成员
  • static修饰的成员被所有对象所共享
  • static优于对象存在,因为static的成员随着类的加载已经存在了
  • static修饰的成员多了一种调用方式,就可以直接被类名所调用。类名.静态成员

弊端

  1. 有些数据是对象特有的数据,是不可以静态修饰的。因为那样的话,特有数据变成对象的共享数据。这样对问题的描述就出了问题。所以在定义静态时,必须明确这个数据是否被对象共享
  2. 静态方法只能访问静态成员,不可以访问非静态成员。因为静态方法加载时,优于对象存在,所以没有办法访问对象中的成员
  3. 静态方法不能使用this,super关键字。因为this代表对象,而静态存在时,有可能没有对象,所以this无法使用

static什么时候用

  1. 成员变量
    当分析对象中所具备的成员变量的值都是相同的;只要这个数据在对象中都是不同的,就是对象的特有数据,必须存储在对象中,是非静态的
  2. 成员函数
    函数是否用静态修饰,就参考一点,就是该函数的功能是否有访问到对象中的特有数据;简单点说就是是否需要访问非静态成员

静态变量

  • 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
  • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。

静态变量与成员变量区别

  1. 两个变量的生命周期不同。
    成员变量随着对象的创建而存在,随着对象的被回收而释放。
    静态变量随着类的加载而存在,随着类的消失而消失。
  2. 调用方式不同。
    成员变量只能被对象调用。
    静态变量可以被对象调用,还可以被类名调用
  3. 别名不同
    成员变量也称为实例变量。
    静态变量称为类变量。
  4. 数据存储位置不同
    成员变量数据存储在堆内存的对象中,所以也叫对象的特有数据.
    静态变量数据存储在方法区(共享数据区)的静态区,所以也叫对象的共享数据.

静态方法

  • 静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法
  • 只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。
  • 主函数是静态的。因为主函数起指挥作用,只负责调用,封装数据,而把功能定义到函数中,函数仿在类中

静态代码块

  • 就是一个有静态关键字标识的代码块区域。定义在类中,随着类的加载而执行
  • 作用:可以完成了的初始化。静态代码块随着类的加载而执行,而且只执行一次(new多个对象也只执行一次)。如果与主函数在同一个类中,优于主函数执行
public class A {
    static {
        System.out.println("123");
    }

    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
    }
}
123  

静态内部类

  • 非静态内部类依赖于外部类的实例,而静态内部类不需要。(?)
  • 静态内部类不能访问外部类的非静态的变量和方法。
public class OuterClass {

    class InnerClass {
    }

    static class StaticInnerClass {
    }

    public static void main(String[] args) {
        // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        StaticInnerClass staticInnerClass = new StaticInnerClass();
    }
}

静态导包

在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

import static com.xxx.ClassName.*

初始化顺序

静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。

public static String staticField = "静态变量";
static {
    System.out.println("静态语句块");
}
public String field = "实例变量";
{
    System.out.println("普通语句块");
}

最后才是构造函数的初始化。

public InitialOrderTest() {
    System.out.println("构造函数");
}

存在继承的情况下,初始化顺序为:

父类(静态变量、静态语句块)
子类(静态变量、静态语句块)
父类(实例变量、普通语句块)
父类(构造函数)
子类(实例变量、普通语句块)
子类(构造函数)
总结:静态、实例、构造
父类 子类

内存图解

在这里插入图片描述

final

为什么要用final修饰变量。
其实在程序如果一个数据是固定的,那么直接使用这个数据就可以了,但是这样阅读性差,所以它该数据起个名称。而且这个变量名称的值不能变化,所以加上final固定。

数据

声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。

对于基本类型,final 使数值不变;
对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。

final int x = 1;
// x = 2;  // 无法将值赋值给最终变量'x'
final A y = new A();//y不能再引用其他对象了
y.a = 1;

方法

声明方法不能被子类重写。

private 方法隐式地被指定为 final,如果在子类中定义的方法和父类中的一个 private 方法签名相同,此时子类的方法不是重写父类方法,而是在子类中定义了一个新的方法。

声明类不允许被继承。

四、 继承

继承的概念

使用关键字extends
父类的由来:由多个类不断向上抽取共性内容而来的
使用继承的好处:

  • 提高了代码的复用性
  • 让类与类产生了关系,提供了另一个特征多态的前提

什么时候继承:
当事物存在所属关系时(is-a)时,把共性抽取,先定义共性,再定义特性

class Person
{
	String name;
	int age;
}
class Student extends/*继承*/ Person
{
//	String name;
//	int age;
	void study()
	{
		System.out.println(name+"...student study.."+age);
	}
}
class Worker extends Person
{
//	String name;
//	int age;
	void work()
	{
		System.out.println("worker work");
	}
}

class ExtendsDemo 
{
	public static void main(String[] args) 
	{
		Student s = new Student();
		s.name= "zhangsan";
		s.age = 22;
		s.study();
	}
}

继承的特点

  1. java中支持单继承。不直接支持多继承,但对C++中的多继承机制进行改良。
    单继承:一个子类只能有一个直接父类。
    多继承:一个子类可以有多个直接父类(java中不允许,进行改良)
    不直接支持,因为多个父类中有相同成员,会产生调用不确定性。
    在java中是通过"多实现"的方式来体现。
  2. java支持多层(多重)继承。C继承B,B继承A。就会出现继承体系。
  3. 当要使用一个继承体系时,
    1,查看该体系中的顶层类,了解该体系的基本功能。
    2,创建体系中的最子类对象,完成功能的使用。

所以,一个体系要想被使用,直接查阅该体系中父类的功能即可知道该体系的基本用法。那么想要使用一个体系时,需要建立对象,建议建立子类对象,因为子类对象不仅使用父类的功能,还可以使用子类特有的功能。
**注意:**子类无法继承父类私有属性,也无法直接访问父类的私有属性。但如果父类中有对私有属性的get和set方法,而且是public修饰的set和get方法,则可以访问。

class Fu
{
	private int num = 4;
	public int getNum()
	{
		return num;
	}
}
class Zi extends Fu
{
	private int num = 5;
	void show()
	{
		System.out.println(this.num+"....."+super.getNum());
	}
}

class ExtendsDemo2 
{
	public static void main(String[] args) 
	{
		Zi z = new Zi();
		z.show();
	}
}

子父类出现后,类中成员都有哪些特点

成员变量

当子父类中出现一样的属性时,子类类型的对象,调用该属性,值是子类的属性值。如果想要调用父类中的属性值,需要使用一个关键字super。这个与this类似。
代表的是子类所属父类中的内存空间引用
注意:子父类中通常不会出现同名成员变量的,因为父类中要定义了,子类就不用定义了,直接继承过来就可以了。

成员函数

当子父类中出现一模一样的方法时,建立该子类对象会运行子类中的方法。好像父类方法被覆盖掉一样。所以这种情况是函数的另一种特性:覆盖(重写)

构造函数

class Fu
{
	int num ;
	Fu()
	{
		num =10;
		System.out.println("A fu run");
	}
	Fu(int x) //重载
	{
		System.out.println("B fu run..."+x);
	}
}
class Zi extends Fu
{
	int num;
	Zi()
	{
		//super();//调用的就是父类中的空参数的构造函数。
		
		System.out.println("C zi run"+num);
	}
	Zi(int x)
	{
		this();
		//super();
//		super(x);
		System.out.println("D zi run "+x);
	}
}
class  ExtendsDemo4
{
	public static void main(String[] args) 
	{
		new Zi(6);
	}
}
class Demo//extends Object
{
	/*
	
	Demo()
	{
		super();
		return;
	}
	*/
}

在子类构造对象时,发现,访问子类构造函数时,父类也运行了。
为什么呢?
原因是:在子类的构造函数中第一行有一个默认的隐式语句。 super();

子类的实例化过程:子类中所有的构造函数默认都会访问父类中的空参数的构造函数。

为什么子类实例化的时候要访问父类中的构造函数呢?
那是因为子类继承了父类,获取到了父类中内容(属性),所以在使用父类内容之前,
要先看父类是如何对自己的内容进行初始化的。

所以子类在构造对象时,必须访问父类中的构造函数。
为什么完成这个必须的动作,就在子类的构造函数中加入了super()语句。

如果父类中没有定义空参数构造函数,那么子类的构造函数必须用super明确要调用
父类中哪个构造函数。同时子类构造函数中如果使用this调用了本类构造函数时,
那么super就没有了,因为super和this都只能定义第一行。所以只能有一个。
但是可以保证的是,子类中肯定会有其他的构造函数访问父类的构造函数。

注意:supre语句必须要定义在子类构造函数的第一行。因为父类的初始化动作要先完成。

由这些特点延伸出继承的三个知识点:

  • super关键字
  • 函数覆盖或重写
  • 子类实例化过程

super关键字

  • 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
  • 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
    (?代码)
public class SuperExample {

    protected int x;
    protected int y;

    public SuperExample(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void func() {
        System.out.println("SuperExample.func()");
    }
}
public class SuperExtendExample extends SuperExample {

    private int z;

    public SuperExtendExample(int x, int y, int z) {
        super(x, y);//使用super来访问父类构造函数,来委托父类来完成一些初始化动作
        this.z = z;
    }

    @Override
    public void func() {
        super.func();
        System.out.println("SuperExtendExample.func()");
    }
}
SuperExample e = new SuperExtendExample(1, 2, 3);
e.func();
SuperExample.func()
SuperExtendExample.func()

函数覆盖(也叫函数重写)

重写(Override)

存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
什么时候使用覆盖:

  • 当一个类的功能需要修改时,可以通过覆盖来实现
  • 当对一个类进行子类扩展时,子类需要保留父类的功能声明,super.method(),但是要定义子类中该功能特有内容时,就使用覆盖操作完成。

为了满足里式替换原则,重写有有以下两个限制:

  • 子类方法的访问权限必须大于等于父类方法;
  • 子类方法的返回类型必须是父类方法返回类型或为其子类型。
    使用 @Override 注解,可以让编译器帮忙检查是否满足上面的两个限制条件。

重载(Overload)

存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。

应该注意的是,返回值不同,其它都相同不算是重载。

子类的实例化过程

上述构造函数内容

访问权限

Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见(同一个包里可以用)。
可以对类或类中的成员(字段以及方法)加上访问修饰符。

  • 类可见表示其它类可以用这个类创建实例对象。
  • 成员可见表示其它类可以用这个类的实例对象访问到该成员;

protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。

设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。

如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。

字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码。

public class AccessExample {
    public String id;
}

可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。

public class AccessExample {

    private int id;

    public String getId() {
        return id + "";
    }

    public void setId(String id) {
        this.id = Integer.valueOf(id);
    }
}

但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。

public class AccessWithInnerClassExample {

    private class InnerClass {
        int x;
    }

    private InnerClass innerClass;

    public AccessWithInnerClassExample() {
        innerClass = new InnerClass();
    }

    public int getValue() {
        return innerClass.x;  // 直接访问
    }
}

抽象类

  • 描述一个事物,却没有足够的信息。这时就将这个事物称为抽象类。
  • 抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
  • 抽象方法一定定义在抽象类中,都需要abstract来修饰。
  • 抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类
  • 只有子类覆盖了所有的抽象方法后,子类具体化,子类就可以创建对象。如果没有覆盖所有的抽象方法,这个子类还是个抽象类
abstract class Employee //抽象类
{
	private String name;
	private String id;
	private double pay;
	Employee(String name,String id,double pay)
	{
		this.name = name;
		this.id = id;
		this.pay = pay;
	}
	public abstract void work();//抽象方法
	
}
//描述程序员。
class Programmer extends Employee
{
	Programmer(String name,String id,double pay)
	{
		super(name,id,pay);//构造函数实例化
	}
	public void work() //覆盖抽象方法
	{
		System.out.println("code...");
	}
}

//描述经理。 
class Manager extends Employee
{
	private int bonus;
	Manager(String name,String id,double pay,int bonus)
	{
		super(name,id,pay); //构造函数实例化
		this.bonus = bonus;
	}
	public void work() //覆盖抽象方法
	{
		System.out.println("manage");
	}
}
class  AbstractTest
{
	public static void main(String[] args) 
	{
		System.out.println("Hello World!");
	}
}

抽象类也是不断向上抽取而来的。抽取了方法的声明而不确定具体的方法类容,又不同的子类来完成具体的方法类容。

抽象类的一些细节

  1. 抽象类中有构造函数吗?
    有,用于给子类对象进行初始化。

  2. 抽象类可以不定义抽象方法吗?
    可以的。 但是很少见,目的就是不让该类创建对象。AWT的适配器对象就是这种类。
    通常这个类中的方法有方法体,但是却没有内容。

	abstract class Demo
	{
		void show1()
		{}
		

		void show2()
		{}
	}
  1. 抽象关键字不可以和那些关键字共存?
    private 不行:因为私有不会被继承
    static 不行:其是随着类的存在而存在的。而abstract一定需要对象的
    final 不行:final规定类不能被继承,方法不可以被覆盖。

  2. 抽象类和一般类的异同点。
    相同点:
    抽象类和一般类都是用来描述事物的,都在内部定了成员。
    不同:
    1,一般类有足够的信息描述事物。
    抽象类描述事物的信息有可能不足。
    2,一般类中不能定义抽象方法,只能定非抽象方法。
    抽象类中可定义抽象方法,同时也可以定义非抽象方法。
    3,一般类可以被实例化。
    抽象类不可以被实例化。

5,抽象类一定是个父类吗?
是的。因为需要子类覆盖其方法后才可以对子类实例化。

接口

定义

  • 当一个抽象类中的方法都是抽象的时候,这时可以将该抽象类用另一种形式定义和表示,就是接口interface
  • 定义接口使用的关键字不是class,而是interface
  • 接口的成员(变量 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。接口的字段默认都是 static 和 final 的。而且这些成员都有固定的修饰符
    全局常量:public static final
    抽象方法:public abstract

实现

  • 类与类之间是继承关系,类与接口之间是实现关系(inplements)
    接口不可以被实例化。只能由实现了接口的子类并覆盖了接口中所有抽象方法后,该子类才可以实例化。否则,这个子类只是个抽象类。
  • 在java中不支持多继承,因为会出现调用的不确定性。所以java将多继承机制进行了改良,在java中变成了多实现。
  • 一个类在继承另一个类的同时,还可以实现多个接口,接口的出现避免了单继承的局限性。可以将类进行功能的扩展。
  • 接口与接口之间是继承关系,而且接口可以多继承。

问:java为什么不能多继承,但可以多实现。
假设A类和B类都有t方法,且具体实现不一样。
C类继承A类和B类,当C类调用t方法时会出现歧义。因为A类和B类都有t方法,但是具体的实现却是不一样的
可以多实现是因为,接口中的方法没有具体的实现。继承多个接口中有相同的方法,也不会出现矛盾。

interface Demo
{
	public static final int NUM = 4;//全局常量

	public abstract void show1(); //抽象方法
	public abstract void show2();
}
class DemoImpl implements Demo
{
	public void show1()
	{}

	public void show2()
	{
	
	}
}


interface A
{
	public void show();
}

interface Z
{
	public int add(int a,int b);
}

class Test implements A,Z  //多实现
{
	
	public int add(int a,int b)
	{
		return a+b+3;	
	}

	public void show(){}
	
}
//一个类在继承另一个类的同时,还可以实现多个接口。
class Q
{
	public void method()
	{}
}

abstract class Test2 extends Q implements A,Z
{

}

//接口的出现避免了单继承的局限性。
interface CC
{
	void show();
}
interface MM
{
	void method();
}

interface QQ extends  CC,MM//接口与接口之间是继承关系,而且接口可以多继承。 
{
	void function();
}

class WW implements QQ
{
//覆盖3个方法。
	public void show(){}
	public void method(){}
	public void function(){}
}
class InterfaceDemo 
{
	public static void main(String[] args) 
	{

		Test t = new Test();
		t.show();

//		DemoImpl d = new DemoImpl();	
//		System.out.println(d.NUM);
//		System.out.println(DemoImpl.NUM);
//		System.out.println(Demo.NUM);
	}
}

接口的特点

  1. 接口是对外提供的规则
  2. 接口是功能的扩展
  3. 接口的出现降低了耦合性

比较(与抽象比较)

  1. 抽象类:一般用于描述一个体系单元,将一组共性内容进行提取,特点:可以在类中定义抽象内容让子类实现,可以定义非抽象内容让子类直接使用。它里面定义的都是体系中的基本内容。
    接口:一般用于定义对象的扩展功能,是在继承之处还需要这个对象具备的一些功能
  2. 抽象类只能被继承,而且是单继承。接口需要被实现,而且是多实现
  3. 抽象类中可以定义抽象方法和非抽象方法,子类继承后,可以直接使用非抽象方法。
    接口中只能定义抽象方法,必须由子类去实现。
  4. 抽象类的继承,是is a关系,在定义该体系的基本共性内容。
    接口的实现是 like a 关系,在定义体系额外功能

五、 多态

概述

  • 函数本身就具备多态性,某一种事物有不同的具体体现。
    一个对象对应着不同类型。例如,猫这类事物即具备着猫的形态,又具备着动物的形态,这就是对象的多态性
  • 多态在代码中的体现,父类引用或者接口引用指向自己的子类对象;父类可以调用子类覆写父类中有的方法。
bstract class Animal
{
	abstract void eat();
}
class Dog extends Animal
{
	void eat()
	{
		System.out.println("啃骨头");
	}
	void lookHome()
	{
		System.out.println("看家");
	}
}
class Cat extends Animal
{
	void eat()
	{
		System.out.println("吃鱼");
	}
	void catchMouse()
	{
		System.out.println("抓老鼠");
	}
}

class Pig extends Animal
{
	void eat()
	{
		System.out.println("饲料");
	}
	void gongDi()
	{
		System.out.println("拱地");
	}
}

class DuoTaiDemo 
{
	public static void main(String[] args) 
	{
		
//		Cat c = new Cat();
//		c.eat();
//		c.catchMouse();
/*
自动类型提升,猫对象提升了动物类型。但是特有功能无法访问。作用就是限制对特有功能的访问.
专业讲:向上转型。将子类型隐藏。就不用使用子类的特有方法。
*/
		Animal a = new Cat(); 
        a.eat();

//如果还想用具体动物猫的特有功能。 你可以将该对象进行向下转型。
//向下转型的目的是为了使用子类中的特有方法。
	Cat c = (Cat)a;
	c.eat();
	c.catchMouse();
//	注意:对于转型,自始自终都是子类对象在做着类型的变化。
	Animal a1 = new Dog();
	Cat c1 = (Cat)a1;//ClassCastException
	}
}

多态的好处

提高了代码的扩展性,前期定义的代码可以使用后期的内容
继承的父类或接口一般是类库中的东西。如果要修改某个地方的具体实现方式,只有通过子类去覆写要改变的某个方法,这样将通过将父类的应用指向子类的实例去调用覆写的方法就行了。
但是这样也存在一些弊端:当父类引用指向子类对象时,虽然提高了扩展性,但是只能访问父类具备的方法,不可以访问子类特有的方法。

多态的前提

  • 必须要有关系,比如继承或者实现
  • 通常会有覆盖操作

多态—转型

向上转型:提高扩展性,限制对特有功能的访问
向下转型:为了子类中的特有方法
多态的出现在思想上也做着变化:以前是创建对象并指挥对象做事情。有了多态以后,我们可以找到多态的共性类型,直接操作共性类型做事情即可,这样可以指挥一批对象做事情,即通过操作父类或接口实现。

多态类型的判断 instance of

instance of 用于判断对象的具体类型,只能用于引用类型判断,通常向下转型前用于健壮性的判断。

多态在子类中的成员上的体现的特点

(?)

  • 成员变量。
    编译时:参考引用型变量所属的类中的是否有调用的成员变量,有,编译通过,没有,编译失败。
    运行时:参考引用型变量所属的类中的是否有调用的成员变量,并运行该所属类中的成员变量。
    简单说:编译和运行都参考等号的左边。
    作为了解。

  • 成员函数(非静态)
    编译时:参考引用型变量所属的类中的是否有调用的函数。有,编译通过,没有,编译失败。
    运行时:参考的是对象所属的类中是否有调用的函数。
    简单说:编译看左边,运行看右边。
    因为成员函数存在覆盖特性。

  • 静态函数
    编译时:参考引用型变量所属的类中的是否有调用的静态方法。
    运行时:参考引用型变量所属的类中的是否有调用的静态方法。
    简单说,编译和运行都看左边。

    其实对于静态方法,是不需要对象的。直接用类名调用即可。

class Fu
{
//	int num = 3;
	void show()
	{
		System.out.println("fu show");
	}

	static void method()
	{
		System.out.println("fu static method");
	}
}

class Zi extends Fu
{
//	int num = 4;
	void show()
	{
		System.out.println("zi show");
	}

	static void method()
	{
		System.out.println("zi static method");
	}
}
class  DuoTaiDemo3
{
	public static void main(String[] args) 
	{
		Fu.method();
		Zi.method();
		Fu f = new Zi();//
//		f.method();
//		f.show();
//		System.out.println(f.num);


//		Zi z = new Zi();
//		System.out.println(z.num);
	}
}

内部类

概述

将一个类定义在另一个类的里面,对里面那个类就称为内部类

object通用方法

定义

所有类的根类,所有类的直接或间接父类,java任务所有对象都具备一些基本的共性内容,这些内容可以不断向上抽取,最终就抽取了一个最顶层的类中,该类中定义就是所有对象都具备的功能。

具体方法

equals

1.等价关系

Ⅰ 自反性

x.equals(x); // true

Ⅱ 对称性

x.equals(y) == y.equals(x); // true

Ⅲ 传递性

if (x.equals(y) && y.equals(z))
    x.equals(z); // true;

Ⅳ 一致性

多次调用 equals() 方法结果不变

x.equals(y) == x.equals(y); // true

Ⅴ 与 null 的比较

对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false

x.equals(null); // false;
2. 等价与相等

对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y); // false
3. 实现
检查是否为同一个对象的引用,如果是直接返回 true;
检查是否是同一个类型,如果不是,直接返回 false;
将 Object 对象进行转型;
判断每个关键域是否相等。

public class EqualExample {

    private int x;
    private int y;
    private int z;

    public EqualExample(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    @Override //帮助自己检查是否正确的复写了父类中已有的方法
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        EqualExample that = (EqualExample) o;

        if (x != that.x) return false;
        if (y != that.y) return false;
        return z == that.z;
    }
}

hashCode()

hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。

在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。

下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。

EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size());   // 2

理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。

一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x,编译器会自动进行这个优化。

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
}

toString()

默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。

public class ToStringExample {

    private int number;

    public ToStringExample(int number) {
        this.number = number;
    }
}
ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
ToStringExample@4554617c

clone()

  1. cloneable

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

public class CloneExample {
    private int a;
    private int b;
}
CloneExample e1 = new CloneExample();
// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'

重写 clone() 得到以下实现:

public class CloneExample {
    private int a;
    private int b;

    @Override
    public CloneExample clone() throws CloneNotSupportedException {
        return (CloneExample)super.clone();
    }
}
CloneExample e1 = new CloneExample();
try {
    CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}

java.lang.CloneNotSupportedException: CloneExample
以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。

应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。

public class CloneExample implements Cloneable {
    private int a;
    private int b;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
  1. 浅拷贝

拷贝对象和原始对象的引用类型引用同一个对象。

public class ShallowCloneExample implements Cloneable {

    private int[] arr;

    public ShallowCloneExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }

    @Override
    protected ShallowCloneExample clone() throws CloneNotSupportedException {
        return (ShallowCloneExample) super.clone();
    }
}
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 222
  1. 深拷贝

拷贝对象和原始对象的引用类型引用不同对象。

public class DeepCloneExample implements Cloneable {

    private int[] arr;

    public DeepCloneExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }

    @Override
    protected DeepCloneExample clone() throws CloneNotSupportedException {
        DeepCloneExample result = (DeepCloneExample) super.clone();
        result.arr = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            result.arr[i] = arr[i];
        }
        return result;
    }
}
DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
  1. clone() 的替代方案

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

public class CloneConstructorExample {

    private int[] arr;

    public CloneConstructorExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public CloneConstructorExample(CloneConstructorExample original) {
        arr = new int[original.arr.length];
        for (int i = 0; i < original.arr.length; i++) {
            arr[i] = original.arr[i];
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }
}
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2

类之间的关系

泛化关系 (Generalization)

用来描述继承关系,在 Java 中使用 extends 关键字。
在这里插入图片描述

@startuml

title Generalization

class Vihical
class Car
class Trunck

Vihical <|-- Car
Vihical <|-- Trunck

@enduml

实现关系 (Realization)

接口:不再是class而是interface
在这里插入图片描述

@startuml

title Realization

interface MoveBehavior
class Fly
class Run

MoveBehavior <|.. Fly
MoveBehavior <|.. Run

@enduml

聚合关系 (Aggregation)

表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。
在这里插入图片描述

@startuml

title Aggregation

class Computer
class Keyboard
class Mouse
class Screen

Computer o-- Keyboard
Computer o-- Mouse
Computer o-- Screen

@enduml

组合关系

和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
在这里插入图片描述

@startuml

title Composition

class Company
class DepartmentA
class DepartmentB

Company *-- DepartmentA
Company *-- DepartmentB

@enduml

关联关系 (Association)

表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。
在这里插入图片描述

@startuml

title Association

class School
class Student

School "1" - "n" Student

@enduml

依赖关系

和关联关系不同的是,依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:

A 类是 B 类方法的局部变量;
A 类是 B 类方法当中的一个参数;
A 类向 B 类发送消息,从而影响 B 类发生变化
在这里插入图片描述

@startuml

title Dependency

class Vihicle {
    move(MoveBehavior)
}

interface MoveBehavior {
    move()
}

note "MoveBehavior.move()" as N

Vihicle ..> MoveBehavior

Vihicle .. N

@enduml
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值