一、面向对象的基本概念
基本概念
面向过程:
所谓面向过程:将要实现的功能描述为从一个开始到结束按部就班的连续步骤;依次逐步完成这些步骤,如果某一步难度较大,又可以将该步骤再次细化为若干子步骤,以此类推,一直到结束得到想要的结果;程序的主体是函数,一个函数就是封装起来的模块,可以实现一定的功能(某一动作),各个子步骤往往就是通过各个函数来完成的,从而实现代码的重用
面向对象:
类,实体,属性,动作(方法)
所谓面向对象就是:在编程的时候尽可能去模拟真实的现实世界,按照现实世界中的逻辑处理问题,分析问题中参与的有哪些实体,这些实体应该有什么属性和方法,我们如何通过这些实体的属性和方法去解决问题。
实体是动作的支配者,没有实体就肯定没有动作发生。任何功能的实现都是依赖于一个具体的实体的"动作,操作,行动"。
面向对象可以看作是一个又一个的实体在发挥其各自“能力“并在内部进行协调有序的调用过程。分析过程为:
- 分析有哪些动作是由哪些实体发出 的
- 定义这些实体,为其增加相应的属性和功能
- 让实体去执行相应的功能和动作
类与对象的关系
类
用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。
过程:
- 执行main方法时,在栈内存中开辟main方法的空间(压栈或进栈),然后在main方法的栈区分配一个变量p2
2.在堆内存中 开辟一个实体空间,分配一个内存首地址(new),完成默认初始化 - 构造函数Person()进栈,name和age进行了构造函数初始化(这两个初始化同时)
- Person()函数出栈
- 初始化后赋给main()函数中的p2,实际是在在堆中的地址p2。
- Speak()进栈
- Speak()出栈
构造函数中的细节
- 构造函数如果完成了set的功能,set是否需要?需要
- 一般函数不能直接调用构造函数
- 构造函数如果前面加了void就变成了一般函数
- 构造函数中是有return语句的
三、关键字
this
- 代表对象,就是所在函数所属对象的引用
- this代表什么呢?
哪个对象调用了this所在的函数,this就代表哪个对象,就是哪个对象的引用 - this使用场景?
当成员变量和局部变量重名的,用关键字this来区分
this的使用场景
- 当成员变量和局部变量重名,可以用关键字this来区分;在定义功能时,如果该功能内部使用到了调用该功能的对象,这时就用this来表示区分。
一个类中,它的所有成员,只要想运行,都必须要有对象调用 - 用this来调用构造函数
- 用this调用构造函数,必须定义在构造函数的第一行。因为构造函数是用于初始化的,所以初始化动作一定要执行,否则编译失败。
不要反复使用this()
- 当成员变量和局部变量重名,可以用关键字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修饰的成员多了一种调用方式,就可以直接被类名所调用。类名.静态成员
弊端
- 有些数据是对象特有的数据,是不可以静态修饰的。因为那样的话,特有数据变成对象的共享数据。这样对问题的描述就出了问题。所以在定义静态时,必须明确这个数据是否被对象共享
- 静态方法只能访问静态成员,不可以访问非静态成员。因为静态方法加载时,优于对象存在,所以没有办法访问对象中的成员
- 静态方法不能使用this,super关键字。因为this代表对象,而静态存在时,有可能没有对象,所以this无法使用
static什么时候用
- 成员变量
当分析对象中所具备的成员变量的值都是相同的;只要这个数据在对象中都是不同的,就是对象的特有数据,必须存储在对象中,是非静态的 - 成员函数
函数是否用静态修饰,就参考一点,就是该函数的功能是否有访问到对象中的特有数据;简单点说就是是否需要访问非静态成员
静态变量
- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
静态变量与成员变量区别:
- 两个变量的生命周期不同。
成员变量随着对象的创建而存在,随着对象的被回收而释放。
静态变量随着类的加载而存在,随着类的消失而消失。 - 调用方式不同。
成员变量只能被对象调用。
静态变量可以被对象调用,还可以被类名调用 - 别名不同
成员变量也称为实例变量。
静态变量称为类变量。 - 数据存储位置不同
成员变量数据存储在堆内存的对象中,所以也叫对象的特有数据.
静态变量数据存储在方法区(共享数据区)的静态区,所以也叫对象的共享数据.
静态方法
- 静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法
- 只能访问所属类的静态字段和静态方法,方法中不能有 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();
}
}
继承的特点
- java中支持单继承。不直接支持多继承,但对C++中的多继承机制进行改良。
单继承:一个子类只能有一个直接父类。
多继承:一个子类可以有多个直接父类(java中不允许,进行改良)
不直接支持,因为多个父类中有相同成员,会产生调用不确定性。
在java中是通过"多实现"的方式来体现。 - java支持多层(多重)继承。C继承B,B继承A。就会出现继承体系。
- 当要使用一个继承体系时,
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!");
}
}
抽象类也是不断向上抽取而来的。抽取了方法的声明而不确定具体的方法类容,又不同的子类来完成具体的方法类容。
抽象类的一些细节
-
抽象类中有构造函数吗?
有,用于给子类对象进行初始化。 -
抽象类可以不定义抽象方法吗?
可以的。 但是很少见,目的就是不让该类创建对象。AWT的适配器对象就是这种类。
通常这个类中的方法有方法体,但是却没有内容。
abstract class Demo
{
void show1()
{}
void show2()
{}
}
-
抽象关键字不可以和那些关键字共存?
private 不行:因为私有不会被继承
static 不行:其是随着类的存在而存在的。而abstract一定需要对象的
final 不行:final规定类不能被继承,方法不可以被覆盖。 -
抽象类和一般类的异同点。
相同点:
抽象类和一般类都是用来描述事物的,都在内部定了成员。
不同:
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);
}
}
接口的特点
- 接口是对外提供的规则
- 接口是功能的扩展
- 接口的出现降低了耦合性
比较(与抽象比较)
- 抽象类:一般用于描述一个体系单元,将一组共性内容进行提取,特点:可以在类中定义抽象内容让子类实现,可以定义非抽象内容让子类直接使用。它里面定义的都是体系中的基本内容。
接口:一般用于定义对象的扩展功能,是在继承之处还需要这个对象具备的一些功能 - 抽象类只能被继承,而且是单继承。接口需要被实现,而且是多实现
- 抽象类中可以定义抽象方法和非抽象方法,子类继承后,可以直接使用非抽象方法。
接口中只能定义抽象方法,必须由子类去实现。 - 抽象类的继承,是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()
- 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();
}
}
- 浅拷贝
拷贝对象和原始对象的引用类型引用同一个对象。
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
- 深拷贝
拷贝对象和原始对象的引用类型引用不同对象。
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
- 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