1 类和对象

类与对象

类(Class)和对象(Object)是面向对象的核心概念。类是对一类事物的描述,是抽象的、概念上的定义。对象是实际存在的该类事物的每个个体,因而也称为实例(instance)。

匿名子类和匿名对象

  • 匿名子类的匿名对象

面向对象原理 - 图1

  • 匿名对象:创建的对象没有显示的赋给一个变量名即为匿名对象。其只能调用一次。

使用情况

  • 如果对一个对象只需要进行一次方法调用,那么就可以使用匿名对象:int age = new Person().getAge();
  • 常将匿名对象作为实参传递给一个方法调用:Test(new Person());

    类常见的成员

  • 属性(也有叫field、字段、成员变量、域)

  • 方法(也有叫method、成员方法、函数)
  • 构造器(构造方法)
  • 代码块
  • 内部类(包括接口,枚举)

    JavaBean

JavaBean是一种Java语言写成的可重用组件,所谓javaBean,是指符合如下标准的Java类:

  1. 类是公共的
  2. 有一个无参的公共的构造器
  3. 有属性,且有对应的get、set方法

用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。

  1. 一个常规的Bean操作
  2. package cn.test.zhuyf;
  3. import java.beans.BeanInfo;
  4. import java.beans.IntrospectionException;
  5. import java.beans.Introspector;
  6. import java.beans.PropertyDescriptor;
  7. import java.lang.reflect.Method;
  8. import org.junit.Test;
  9. public class JavaBeanTest {
  10. /*得到bean对象的所有属性*/
  11. @Test
  12. public void GetBeanInfo() throws IntrospectionException{
  13. BeanInfo beaninfo=Introspector.getBeanInfo(JavaBean.class,Object.class);
  14. PropertyDescriptor[] props= beaninfo.getPropertyDescriptors();
  15. for (PropertyDescriptor prop : props) {
  16. System.out.println(prop.getName());;
  17. }
  18. }
  19. /*操作bean对象的指定属性值:Age*/
  20. @Test
  21. public void GetBeanProperty() throws Exception{
  22. //初始化
  23. JavaBean j=new JavaBean();
  24. PropertyDescriptor prop=new PropertyDescriptor("age", JavaBean.class);
  25. //读操作
  26. Method method= prop.getWriteMethod();
  27. method.invoke(j,45);
  28. System.out.println(j.getAge());
  29. //写操作
  30. method=prop.getReadMethod();
  31. Object obj= method.invoke(j, null);
  32. System.out.println(obj);
  33. }
  34. /*操作bean对象的指定属性类型:Age*/
  35. @Test
  36. public void GetBeanPropertyType() throws Exception{
  37. //初始化
  38. JavaBean j=new JavaBean();
  39. PropertyDescriptor prop=new PropertyDescriptor("age", JavaBean.class);
  40. System.out.println(prop.getPropertyType());
  41. }
  42. }

UML类图

面向对象原理 - 图2

2 类的五大成员

2.1 构造器(构造方法)

构造器的特征

  • 它具有与类相同的名称。
  • 它不声明返回值类型(与声明为void不同)。
  • Java 语言中,每个类都至少有一个构造器,系统默认提供的构造器的修饰符与所属类的修饰符一致,一旦显式的定义了构造器,则系统不再提供默认的构造器。
  • 不能被static、final、synchronized、abstract、native修饰,不能有return语句返回值。
  • 父类的构造器不可被子类继承。

    作用&格式和分类

  • 构造器的作用:创建对象并给对象进行初始化

  • 语法格式

    1. 修饰符 类名(参数列表) {
    2. 初始化语句;
    3. }

    记忆:public person(String name){} => 在new的时候new Person(“He”);其前后是对应的。

  • 构造器的分类:隐式无参构造器(系统默认提供)、显式定义一个或多个构造器(无参、有参)。

    2.2 属性

    属性也有叫field、字段、成员变量、域,是类的一个重要成员。

    属性VS局部变量

相同点:定义变量的格式相同;均需要先声明后使用;均有其对应的作用域。
不同点

属性(成员变量) 局部变量
使用地方不同 在方法体外,类体内
直接定义在{}内,属于类成员
声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量等
权限修饰符使用不同 可以指定权限修饰符
private/public/default/protected等
不可使用权限修饰符
默认初始化值不同 有默认值
整形(byte/short/int/long):0
浮点型(float/double):0.0
字符型(char):0或’\u0000’
布尔型(boolean):false
引用类型(类含String/数组/接口):null
没有默认初始化值
意味着,我们在调用局部变量之前,一定要显示赋值。
在内存中加载的位置不同 堆空间(非static)
方法区静态域(static)
栈空间
面向对象原理 - 图3

属性赋值的先后顺序

类型默认初始化(int值为0)(第一步)->显示初始化值和代码块顺序执行(第二步)->构造器中初始化(第三步)->方法手动更改设置

  1. public class Person{
  2. int age=21;//第二步
  3. {
  4. age=22;//第二步:显示初始化和代码块内顺序执行,谁先在前面谁先执行
  5. }
  6. Person(){
  7. age=3;//第三步
  8. }
  9. }

面向对象原理 - 图4

2.3 方法

基本概念:方法,也有叫method、成员方法、函数,是类的重要成员
格式:

  1. 修饰符 返回值类型 方法名(参数类型 形参1,参数类型 形参2, …){
  2. 方法体程序代码
  3. return返回值;
  4. }

方法的重载(overload)

在同一个类中,允许存在一个以上的同名方法,只要存在如下任意一个即为方法的重载

  • 参数个数不同
  • 参数个数相同但存在对应位置的参数的数据类型不同

简单一句话:重载只根据方法的参数列表来区别,跟权限修饰符、返回值类型、形参变量名、方法体都没有关系。其实从逻辑上将,编译器只能通过参数列表来区分同名的方法。

  1. int[] arr = new int[10];
  2. System.out.println(arr);

输入值:地址值
面向对象原理 - 图5

  1. char[] arr1 = new char[10];
  2. System.out.println(arr1);

输入值:具体的char值
面向对象原理 - 图6
原理分析:系统提供的print()方法针对char[]数组进行了重写,其输出的就是char[]数组各个值的拼接结果集。如果是String[] arrs={“a”,”b”};println(arrs);则调用的是print(Object)其打印的结果就是个地址值。
面向对象原理 - 图7

可变个数的形参

概述

JavaSE5.0 中提供了Varargs(variable number of arguments)机制,允许直接定义能和多个实参相匹配的形参。从而,可以用一种更简单的方式,来传递个数可变的实参。其本质就是数组,传入和使用可以和数组的使用一致。

  1. //JDK5.0以前:采用数组形参来定义方法,传入多个同一类型变量
  2. public static void test(int a,String[] books);
  3. //JDK5.0:采用可变个数形参来定义方法,传入多个同一类型变量
  4. public static void test(int a,String books);

几个特征

  1. 本质即为数组的语法糖,参数传入,参数使用均可以和数组互通
  2. 方法参数部分指定类型的参数个数是可变多个:0个,1个或多个

    1. public static void Test(Person... p){
    2. }
    3. 调用处:可以传2个参数Test(new Person(), new Person());也可以不传入Test();或传入数组格式Test(new Person[2]);
  3. 可变个数形参的方法与同名的方法之间,彼此构成重载

如下是两个重载方法:public static void Test(Person... p){}和public static void Test(Person p){}
但是不能这样做:public static void Test(Person... p) {}和public static void Test(Person[] p) {}
提示:原因如上JDK5.0前后的区别所示(故本质上可变参数和一维数组一致)
面向对象原理 - 图8

  1. 可变参数方法的使用与方法参数部分使用数组是一致的

    1. public static void Test(Person... parrs) {
    2. //计算传入的可变参数值
    3. System.out.println(parrs.length);
    4. for(int i=0;i<parrs.length;i++){
    5. sout(parrs[i].getAge);
    6. }
    7. }
  2. 方法的参数部分有可变形参,需要放在形参声明的最后

  3. 在一个方法的形参位置,最多只能声明一个可变个数形参

    参数的传值机制

    实参:调用处的参数
    形参:方法的参数

Java变量间的赋值和方法间的参数是如何传递的?
Java变量间的赋值和方法间的参数传递方式只有一种:值传递,传递的是栈空间上存储的值。如果被赋值或被传递的数据是值(基本)类型:由于栈空间存储的是其具体的值,此时传递的是具体的”数据值”。如果被赋值或被传递的是引用类型:由于栈空间存储的是引用类型的地址,此时传递的是”数据地址”。该机制和C#一样。

2.4 代码块

代码块的作用和分类:用来初始化类、对象。
格式:修饰符要么为空,要么为static,不能有其他修饰符
划分:分为静态代码块和非静态代码块

分类 概述
静态代码块
- 随着类的加载而执行,只执行一次,常用于初始化类的信息,其执行要优先于非静态代码块
- 块内只能调用静态的属性、静态的方法,不能调用非静态的结构,其内部可以有输出语句
- 如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行
非静态代码块
- 随着对象的创建而执行,可以在创建对象时,对对象的属性等进行初始化。优先于构造器执行
- 块内可以调用静态的属性、静态的方法,或非静态的属性、非静态的方法,内部可以有输出语句
- 如果一个类中定义了多个非静态代码块,则按照声明的先后顺序执行

2.5 内部类

定义:Java中允许将一个类A声明在另一个类B中,则类A就是内部类,类B称为外部类

分类

类别 细分
成员内部类 静态成员内部类 其作为外部类的一个成员的角度:
- 可以调用外部类的结构
- 可以被static修饰
- 可以被4种不同的权限修饰


其作为一个类的角度:
- 类内可以定义属性、方法、构造器等
- 其内部类本分可以被final修饰,表示此类不能被继承(即不使用final,该内部类可以被继承)
- 可以被abstract修饰
非静态成员内部类
局部内部类 无修饰符,可定义在方法内、代码块内、构造器

如何在外部实例化类的成员变量

  1. //创建Person里的Brain实例(Brain是静态的成员内部类):
  2. Person.Brain brain = new Person.Brain();
  3. brain.think();
  4. //创建Person里的Hand实例(Hand是非静态的成员内部类):
  5. //Person.Hand hand = new Person.Hand();//错误的
  6. Person p = new Person();
  7. Person.Hand hand= p.new Hand();
  8. hand.move();

如何在成员内部类中区分调用外部类的结构

  1. System.out.println(name);//方法的形参
  2. System.out.println(this.name);//内部类的属性(this.内部类属性)
  3. System.out.println(Person.this.name);//外部类的属性(外部类.this.外部类的属性)
  4. //如果内部类和外部类的属性不重复,则可以省略this和外部类.this

3 封装

四种权限符修饰:public、protected、(缺省)、private置于类的成员定义前,用来限定对象对该类成员的访问权限。
面向对象原理 - 图9

  • public类可以在任意地方被访问;default类只可以被同一个包内部的类访问。
  • 对于类的内部结构如方法、属性、构造器、内部类,4个权限修饰符均可以使用;对于class的权限修饰只可以用public和default(缺省)。

    4 继承

    4.1 基本概念

    继承的好处:减少了代码的冗余,提高了代码的复用性;便于功能的扩展;为多态性的使用,提供了前提。
    继承的格式:class A extends B{ },其中A也称为子类、派生类、SubClass,b称为父类、超类、基类、SuperClass。
    特征:

  • 一旦子类A继承父类B以后,子类A中就获取了父类B中声明的所有的属性和方法。特别的,父类中声明为private的属性或方法,子类继承父类以后,仍然认为获取了父类中私有的结构。只是因为封装性的影响,使得子类不能直接调用父类的结构而已。原理记忆:子类调用父类可访问的属性和方法=>父类对应的属性和方法内部在去访问其私有的方法和属性=>可访问到。

  • Java是单继承性:一个类只能有一个父类(C++支持多继承性)
  • 子类继承父类以后,就获取了直接父类以及所有父类中声明的属性和方法
  • 所有的java类(除Object类本身之外)都直接或间接的继承于java.lang.Object类,所有的java类具有java.lang.Object类声明的功能。

    4.2 override重写

    定义

在子类中可以根据需要对从父类中继承来的方法进行改造,也称为方法的重置、覆盖。在程序执行时,子类对象的方法将覆盖父类对象的方法。
子类重写父类的方法。可用来打标签的地方:当前类的实例方法。
简易总结:在重写的时候,尽量保持修饰符、参数名、返回值类型那、参数列表一致。重写只针对对象有效,针对类无效。
注意:类的属性没有覆盖之说,子类和父类可以同时存在相同类型和名字的属性,并且子类对象在内存里确实会存在两个属性名。

重写方法的格式要求

子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表,@Override可省略,但不建议这样做。

  1. //父类方法:
  2. public int getAge() {}
  3. //子类对该父类方法进行重写
  4. @Override
  5. public int getAge() {
  6. //方法体
  7. }

重写要求

场景 规范
对权限修饰符的要求
- 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限,即如果父类的权限是protected,则子类必须是protected或public。原因分析:因为子类要覆盖父类的方法,如果权限变小,则达不到重写覆盖的效果。
- 子类不能重写父类中声明为private权限的方法(子类此时可以写和父类一样的方法和参数列表,只是和父类不在是重写与被重写关系)。
对返回值的要求
- 父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是void
- 父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类(要达到覆盖效果)
- 父类被重写的方法的返回值类型是基本数据类型(比如:double),则子类重写的方法的返回值类型必须是相同的基本数据类型(必须也是double)
对方法体的要求
- 如果父类有抛出异常,子类重写的方法体可以不抛出异常。但是如果子类重写的方法有抛出的异常的话,其必须为父类抛出异常的类或其子类。
其他要求
- 子类与父类中同名同参数的方法必须同时声明为非static的(即为可重写),或者同时声明为static的(即不可重写的)。因为static方法是属于类的,子类无法覆盖父类的方法。

面向对象原理 - 图10 |

4.3 abstract抽象

  • abstract可以用来修饰的结构:类、方法,abstract天然就是用来继承和重写的;不能用来修饰变量、代码块、构造器、私有方法、静态方法、final方法、final类、接口等。

可以简单理解:抽象类就是比普通类多定义了抽象方法,除了不能直接进行类的实例化操作之外,并没有任何的不同(当然抽象类里面可以没有抽象方法)。

  • abstract修饰的类即为抽象类
    • 此类不能实例化
    • 抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化的全过程)
    • 开发中,都会提供抽象类的子类,让子类对象实例化,完成相关的操作:abstract class Fly{}
  • abstract修饰方法即为抽象方法

    • 抽象方法只有方法的声明,没有方法体public abstract void eat();
    • 包含抽象方法的类一定是一个抽象类(防止方法被实例化调用)。反之,抽象类中可以没有抽象方法
    • 只有子类重写了父类中的所有的抽象方法后,此子类才可实例化。若子类没有重写父类中的所有的抽象方法,则此子类也是一个抽象类,需要使用abstract进行修饰。

      4.4 final最终

  • final可以用来修饰的结构:类、方法、变量、内部类,均不限定是否为静态的类、方法或变量,不可修饰构造器、代码块

  • final修饰类:此类不能被其他类所继承,如String类、System类、StringBuffer类:final class A{}//此类不可继承
  • final修饰方法:表明此方法不可以被重写,如Object类中getClass()

    1. //此方法即不可重写override
    2. public final void print() {
    3. System.out.println("A");
    4. }
  • final修饰属性:用final修饰的属性必须在声明时或在每个构造器中或代码块中初始化显式赋值后才能使用,一旦初始化赋值后即不可再更改了。

注:常量名要大写private final String NAME = "peiqi";

  • final修饰局部变量:尤其是使用final修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参。一旦赋值后就只能在方法体内使用此形参而无法重新赋值。

    1. public void show(final int num){
    2. num = 20;//方法内不可更改,编译不通过
    3. System.out.println(num);
    4. }
  • static final 用来修饰属性:全局常量(相当于const)

    4.5 this和super

    this当前调用者对象

    定义:this可以在当前类的非静态方法、构造器、非静态代码块、内部类中用来调用当前类对象的属性(包静态和非静态)、方法(含静态和非静态)和构造器。

  1. 在方法或构造器中调用当前类对象的其他方法或属性

在类的构造器或方法中,我们可以使用"this.属性""this.方法"的方式调用当前类对象的方法和对象。
通常情况下,我们都选择省略”this.”,如果属性和方法名在父类和子类同时存在,即默认this,而不是super。特殊情况下,如果方法的形参和类的属性同名时,我们必须使用”this.变量”的方式,表明此变量是属性,而非形参。
辅助说明:类文件里面在方法/构造器/内部类/代码块里直接调用方法名或属性名的要么就是前面省略了this(实例对象)、要么就是前面省略了类(静态)。

  1. 在构造器中调用当前类对象的其他构造器

    1. 我们在类的构造器中,可以显式的使用”this(形参列表)”方式,调用本类中指定的其他构造器
    2. 不能通过”this(形参列表)”方式调用自己
    3. 如果一个类中有n个构造器,则最多有 n - 1构造器中使用了”this(形参列表)”
    4. 规定:”this(形参列表)”必须声明在当前构造器的首行(同时也就表明了一个构造器最多只能通过this的方式调用一次当前类对象的其他构造器)
    5. 使用this访问属性和方法时,如果在本类中未找到,会从父类中查找

      super当前对象的父类对象

      定义:super可用于在当前类的非静态方法、构造器、非静态代码块、内部类中调用父类的属性(含静态和非静态)、成员方法(含静态和非静态)和构造器。
  2. 在非静态方法或构造器中调用父类的属性和方法

我们可以在子类的非静态方法或构造器中。通过使用"super.属性""super.方法"的方式,显式的调用父类中声明的属性或方法,其中方法不局限于静态与否。通常情况下,我们习惯省略”super.”,如果属性或方法名在父类和子类里同时存在,则默认this,而不是super。
特殊情况一:当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则必须显式的使用”super.属性”的方式,表明调用的是父类中声明的属性。
特殊情况二:当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用”super.方法”的方式,表明调用的是父类中被重写的方法。

  1. 在构造器里调用父类的构造器
    1. 我们可以在子类的构造器中显式的使用”super(形参)”的方式,调用父类中声明的指定的构造器
    2. “super(形参列表)”的使用,必须声明在子类构造器的首行,此规则就决定了:必须只能调用一个super()构造器且针对于”this(形参列表)”或”super(形参列表)”只能二选一,不能同时出现
    3. 在构造器的首行,没有显式的声明”this(形参列表)”或”super(形参列表)”,则默认调用的是父类中空参的构造器:super()
    4. 在类的多个构造器中,至少有一个类的构造器中使用了”super()/super(参数列表)”去调用父类中的构造器。

推导过程:已知类的构造器首行要么是this(),要么是super(),要么就是默认不显示的super(),而一个类不可能所有的构造器都写成this本类其他构造器(如果这样,则循环this初始化了),
那么就肯定至少有一个是super父类构造器的

  1. 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员,super的追溯不仅限于直接父类
  2. super和this的用法相像,this代表本类对象的引用,super代表父类内存空间的标识(不一定是对象,因为可以调用父类的静态方法)

    关于this和super的使用区别

    面向对象原理 - 图11

    重点:子类对象的实例化过程

  3. 从结果上来看

子类继承父类以后,就获取了父类中声明的属性或方法。创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。

  1. public class ClassTest {
  2. public static void main(String[] args) {
  3. Student student = new Student();
  4. student.sayAge();
  5. }
  6. }
  7. class Student extends Person {
  8. public void sayAge() {
  9. System.out.println(age);
  10. sayHi();
  11. }
  12. }
  13. class Person {
  14. public int age = 10;
  15. public void sayHi() {
  16. System.out.println("Hi");
  17. }
  18. }
  19. //输出结果
  20. 10
  21. Hi
  1. 从过程上来看

当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,直到调用了java.lang.Object类中空参的构造器为止。正因为加载过所有的父类的结构,所以才可以看到内存中有父类中的结构,子类对象才可以考虑进行调用。
明确:虽然创建子类对象时,调用了父类的构造器,但是自始至终就创建过一个对象,即为new的子类对象。
面向对象原理 - 图12
一个例子

  1. //Father类
  2. public class Father {
  3. public Father(){
  4. System.out.println("Father构造器");
  5. }
  6. public void run() {
  7. sing();
  8. System.out.println("father.run");
  9. }
  10. }
  11. //Son类
  12. public class Son extends Father {
  13. public Son(){
  14. System.out.println("Son构造器");
  15. }
  16. @Override
  17. public void sing() {
  18. System.out.println("son.sing");
  19. }
  20. @Override
  21. public void run() {
  22. super.run();
  23. System.out.println("son.run");
  24. }
  25. }
  26. //有如下代码,请输出值
  27. @Test
  28. public void Test() {
  29. Son son = new Son();
  30. son.run();
  31. }

结果:
面向对象原理 - 图13
结果分析:
面向对象原理 - 图14

5 多态

基本概念

多态性,是面向对象中最重要的概念。在Java中的体现:声明父类,实例化子类。
几个特征:

  1. 多态分为编译阶段和运行阶段,若编译时类型和运行时类型不一致,就出现了对象的多态性
    1. 多态只针对子类重写的方法有效,在编译的时候,编译器看左边(所以在编译的时候变量就不能再访问子类中添加的属性和方法—编译器通不过),运行的时候解释器看右边。

一定要注意:是子类重写的方法,如果子类有可变参数格式一样的但不是重写的,也不能访问。

  1. 对于成员变量(属性),其不具备多态性。此时访问和使用属性,编译运行均看左边的类型。

例子:类Person有属性name,方法sayHello(),其子类Student也有同名的name和重写的sayHello()方法。如果Person p=new Student();此时p.name在运行时调用的是Person类的
name(因为属性没有多态),p.sayHello()在运行时调用的是Student子类的sayHello(只有方法才有多态性)。特别需要说的是:Person p=new Student();该语句变量p实际生成的是
Student对象,那么Student对象里面的所有属性和方法都在内存里,部分属性p不能使用到是因为编译器通不过的问题,不是内存的问题。

  1. 子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。

即:对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或”动态绑定”。
一个面试题:多态是编译时行为还是运行时行为?答案:运行时。

一道经典的多态题目

  1. public class InterviewTest1 {
  2. public static void main(String[] args) {
  3. Base1 base = new Sub1();
  4. base.add(1, 2, 3);
  5. Sub1 s = (Sub1)base;
  6. s.add(1,2,3);
  7. }
  8. }
  9. class Base1 {
  10. public void add(int a, int... arr) {
  11. System.out.println("base1");
  12. }
  13. }
  14. class Sub1 extends Base1 {
  15. public void add(int a, int[] arr) {
  16. System.out.println("sub_1");
  17. }
  18. public void add(int a, int b, int c) {
  19. System.out.println("sub_2");
  20. }
  21. }

考察输出的值
面向对象原理 - 图15

6 面向对象的关键字

6.1 package(类所属的包)

  • 为了更好的实现项目中类的管理,提供包的概念使用package声明类或接口所属的包,声明在源文件的首行
  • 包的命名统一使用小写
  • 每”.”一次,就代表一层文件目录
  • 同一个包下,不能命名同名的接口、类。不同的包下,可以命名同名的接口、类

    JDK 中主要的包介绍

6.2 import(导入包下的类)的使用

  • 在源文件中显式的使用import结构导入指定包下的类、接口,其声明在包的声明和类的声明之间。其导入的是包下的类

面向对象原理 - 图16

  • 可以使用"xxx.*"的方式,表示可以导入xxx包下的所有结构。但是如果使用的是xxx子包下的结构,则仍需要显式导入
  • 如果使用的类或接口是java.lang包下定义的,则可以省略import结构:java.lang为Java的核心包如String/System类
  • 如果使用的类或接口是本包下定义的,则可以省略import结构
  • 如果在源文件中,使用了不同包下的同名的类,则必须至少有一个类需要以全类名的方式显示
  • import static:导入指定类或接口中的静态结构=>属性或方法

    1. //案例:如下类有一个静态方法
    2. package sub;
    3. public class Person {
    4. public static void SayHello() {
    5. System.out.println("Hello");
    6. }
    7. }
    8. //其他类就可以通过import static的方式导入刚才类的静态方法和属性,注意导入的是不再是类,而是属性和方法
    9. import static sub.Person.*;
    10. public class Main {
    11. public static void main(String[] args) {
    12. sub.Person.SayHello();
    13. //导入后可以用这种快捷的方法调用
    14. SayHello();
    15. }
    16. }

    6.3 instanceof(实例-类)

    为了避免在向下转型时出现ClassCastException的异常,我们在强转数据前,先进行instanceof判断。
    格式:x instanceof A,检验x是否为类A的对象,返回值为boolean 型。

  • 要求x所属的类与类A必须是子类和父类的关系,否则编译错误。

  • 如果x属于类A的子类B,x instanceof A值也为true。

如下代码均为正确,因为任何类都是Object的子类。Object此处类似于中国,p类似于具体的区/街道之类

  1. if(p instanceof Object){
  2. System.out.println("******Object******");
  3. }

6.4 static(静态)

应用场景

在Java类中,可用static修饰属性、方法、代码块、内部类,注意:Java里,static不能修饰类/构造器。被修饰的成员优先于对象而存在,其随着类的加载而加载,存在于方法区的静态区,可被所有的对象所访问(要在访问权限范围内)

静态属性

使用static修饰的属性称为静态变量(或类变量),被static修饰的属性有如下特征

  • 静态变量随着类的加载而加载。可以通过”类.静态变量”或”对象.静态变量”的方式进行调用
  • 静态变量的加载要早于对象的创建
  • 由于类只会加载一次,则静态变量在内存中也只会存在一份:存在方法区的静态域中。

    静态方法

  • 随着类的加载而加载,可以通过”类.静态方法”或”对象.静态方法”的方式进行调用

  • 静态方法中,只能调用静态的方法或属性(除非在静态方法里面手动new一个对象)
  • 在静态的方法内,不能使用this关键字、super关键字():编译会不通过,从Java的角度,this/super均指的是对象的内存地址

    6.5 interface(接口)

    概念和特征
    接口的本质是契约、标准,相对于类继承间的is-a的关系,接口更多的是has-a的关系。在Java中,接口和类是并列的两个结构,从本质上讲,接口是一种特殊的抽象类。
    类可以实现多个接口(弥补了Java单继承性的局限性)。
    接口中的成员(JDK7.0及之前)
    JDK7及以前:只能定义全局常量和抽象方法。
    全局常量:必须使用public static final修饰,书写时可省略。
    抽象方法:必须使用public abstract修饰,书写时候可以省略。
    不管任何JDK版本,接口中均不能定义构造器、代码块,即意味着接口不可以实例化。
    接口中的成员(JDK8.0)
    JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法。从技术角度来说,这是完全合法的,只是它看起来违反了接口作为一个抽象定义的理念。
    静态方法:使用static关键字修饰。可以通过接口直接调用静态方法,并执行其方法体。我们经常在相互一起使用的类中使用静态方法。可以在标准库中找到像Collection/Collections或者Path/Paths这样成对的接口和类。static void eat(){System.out.println(“eat”);}
    默认方法:默认方法使用 default 关键字修饰。可以通过实现类对象来调用。我们在已有的接口中提供新方法的同时,还保持了与旧版本代码的兼容性。如:java 8 API中对Collection、List、Comparator等接口提供了丰富的默认方法。
    JDK8.0接口的几个特征

  • 【静态方法】接口中定义的静态方法,只能通过”接口.静态方法”的方式来调用。通过接口的实现类或接口的实现类对象或接口的继承接口的静态方法/默认方法均不能访问。(简言之:接口中的静态方法只能接口本身自己用)。此功能可以让接口成为一个工具类。

面向对象原理 - 图17

  • 【静态方法】通过实现类的对象,可以调用接口中的默认方法(由于默认方法是非static的,故只能是对象才能调用),默认方法可以override重写,如果实现类重写了接口中的默认方法,调用时,调用的则是重写后的方法
  • 【默认方法-类优先】如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法。—>类优先原则
  • 【默认方法-接口冲突】实现了多个接口,而这多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,报错。—>接口冲突。
  • 【静态&默认方法】如何在子类(或实现类)的方法中调用父类、接口中被重写的方法

    1. sayHello();//调用自己定义的重写的方法
    2. super.sayHello ();//调用的是父类中声明的
    3. //调用接口中的默认方法(默认方法均是非静态的)
    4. IFlyable.super.sayHello();
    5. //调用接口中的静态方法
    6. IFlyable.sayHello();

    接口—类—接口之间的关系

  • 类实现接口:实现(implements)

Java开发中,接口通过让类去实现(implements)的方式来使用。如果实现类覆盖了接口中的所有抽象方法,则此实现类就可以实例化。如果实现类没有覆盖接口中所有的抽象方法,则此实现
类仍为一个抽象类。

  1. public class FlySon implements IFlyable{}//实现接口或
  2. public class FlySon extends Fly implements IFlyable{}//继承父类且又实现接口
  • 接口继承接口:继承(且可以多继承)

格式:public interface IFlyable extends IManable,IPersonable{}
另:使用匿名类的匿名对象来传递接口的对象

  1. //定义接口
  2. public interface IFlyable {
  3. int SPEED = 100;
  4. //声明+初始化
  5. ArrayList arrs = new ArrayList();
  6. void fly();
  7. }
  8. //在第三方调用
  9. IFlyable flyable = new IFlyable() {
  10. @Override
  11. public void fly() {
  12. System.out.println("Hello");
  13. }
  14. };

抽象类和接口之间的异同
面向对象原理 - 图18
一道例子

public class Fly{ int value = 10; } public interface IFlyable { int value = 20; } //具体使用类 public class FlySon extends Fly implements IFlyable{ public void fly() { System.out.println(value);//此语句会报错 //报错内容:父类和接口名相同,编译器无法识别 面向对象原理 - 图19 //如何调用父类的value—调用父类对象 int x= super.value; //如何调用接口的value—因为接口修饰属性都是public static final int interface_value=IFlyable.speed; } }

9 原理之类的编译运行与内存结构

9.1 类的编译与运行过程(数据+方法逻辑)

参考地址:https://www.cnblogs.com/qiumingcheng/p/5398610.html
有如下的一个类

  1. //MainApp.java
  2. public class MainApp {
  3. public static void main(String[] args) {
  4. Animal animal = new Animal("Puppy");
  5. animal.printName();
  6. }
  7. }
  8. //Animal.java
  9. public class Animal {
  10. public String name;
  11. public Animal(String name) {
  12. this.name = name;
  13. }
  14. public void printName() {
  15. System.out.println("Animal ["+name+"]");
  16. }
  17. }

类的编译

程序会将类文件编译成.class文件,编译后的字节码文件格式主要分为两部分:常量池和方法字节码。
常量池记录的是代码出现过的所有token(类名、成员变量名等等)以及符号引用(方法引用、成员变量引用等等)。
面向对象原理 - 图20
方法字节码放的是类中各个方法的字节码。
面向对象原理 - 图21

类的运行

Java类运行的过程大概可分为两个过程:1、类的加载2、类的执行。需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是一开始就把一个程序的所有的类都加在到内存中,而是到不得不用的时候才加载进来,而且只加载一次。
有如下的程序:

  1. public class MainApp {
  2. public static void main(String[] args) {
  3. Animal animal=new Animal();
  4. animal.printName();
  5. }
  6. }

下面是程序运行的详细步骤:

  1. LoadClass:加载入口函数在编译好java程序得到MainApp.class文件后,在命令行上敲java

MainApp。系统就会启动一个jvm进程,jvm进程从classpath路径中找到一个名为MainApp.class的二进制文件,将MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。

  1. 运行入口Main函数:然后JVM找到AppMain的主函数入口,开始执行main函数。
  2. 加载Animal类(类唯一对象,存放在方法区里面):main函数的第一条命令是Animal animal = new Animal(“Puppy”);就是让JVM创建一个Anima对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到方法区中。注意这个不是new出来的对象,而是类的类型对象,每个类只有一个class对象,作为方法区类的数据结构的接口,可以通过反射得到该类对象(使用Animal.class或Class.forName(“包.类”));
  3. 实例化Animal对象(实例对象,堆空间):加载完Animal类之后,Java虚拟机做的第一件事情就是在

堆区中为一个新的Animal实例分配内存, 然后调用构造函数初始化Animal实例,这个Animal实例持有着指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用。

  1. 类唯一实例对象+实例对象:当使用animal.printName()的时候,JVM根据animal引用找到Animal

对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数的字节码的地址。

  1. 开始运行printName()函数。

面向对象原理 - 图22

9.2 类的加载过程

面向对象原理 - 图23
面向对象原理 - 图24
面向对象原理 - 图25

9.3 JVM内存结构(数据+逻辑)

面向对象原理 - 图26
运行(.class文件)的时候才会去分配内存空间

驻留于常规 RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆
栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存
方式,仅次于寄存器。创建程序时,Java 编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存
在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活
性,所以尽管有些Java 数据要保存在堆栈里——特别是对象句柄,但Java 对象并不放到其中。
简易总结:栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、
char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,是对象在堆内存的首地址)。方法执行完,自动释放。

一种常规用途的内存池(也在 RAM区域),其中保存了Java 对象。和堆栈不同,“内存堆”或
“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要
在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命
令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然
会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!
简易总结: 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在
Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。

  1. 方法区

方法区用于存储已被虚拟机加载的类信息、常量(常量池)、静态变量、即时编译器编译后的代码、方法表等数据。

  1. 方法区之静态存储

这儿的“静态”(Static)是指“位于固定位置”(尽管也在 RAM里)。程序运行期间静态存储的数据将随时等候调用。可用static 关键字指出一个对象的特定元素是静态的。

  1. 方法区之常数存储

常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。
有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。

9.4 JVM数据存储剖析(数据)

面向对象原理 - 图27
面向对象原理 - 图28
C++认为程序的执行效率是最重要的一个问题,所以它允许程序员作出选择。为获得最快的运行速度,存储以及存在时间可在编写程序时决定,只需将对象放置在堆栈(有时也叫作自动或定域变量)或者静态存储区域即可。这样便为存储空间的分配和释放提供了一个优先级。某些情况下,这种优先级的控制是非常有价值的。然而,我们同时也牺牲了灵活性,因为在编写程序时,必须知道对象的准确的数量、存在时间、以及类型。如果要解决的是一个较常规的问题,如计算机辅助设计、仓储管理或者空中交通控制,这一方法就显得太局限了。
程序数据保存在哪里?(来源于Java编程思想第四版)
程序运行时,我们最好对数据保存到什么地方做到心中有数。特别要注意的是内存的分配。有六个地方都可
以保存数据:寄存器、栈、堆、方法区(常量区+静态区)、非RAM

  1. 寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部。然而,寄存

器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对此没有直接的控制权,也不可能在自己的
程序里找到寄存器存在的任何踪迹。

  1. 栈。驻留于常规 RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆

栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存
方式,仅次于寄存器。创建程序时,Java 编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存
在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活
性,所以尽管有些Java 数据要保存在堆栈里——特别是对象句柄,但Java 对象并不放到其中。
简易总结:栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、
char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,是对象在堆内存的首地址)。方法执行完,自动释
放。

  1. 堆。一种常规用途的内存池(也在 RAM区域),其中保存了Java 对象。和堆栈不同,“内存堆”或

“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要
在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命
令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然
会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!
简易总结: 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在
Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。

  1. 方法区之静态存储。这儿的“静态”(Static)是指“位于固定位置”(尽管也在 RAM里)。程序运行

期间静态存储的数据将随时等候调用。可用static 关键字指出一个对象的特定元素是静态的。

  1. 方法区之常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。

有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。
扩展:方法区用于存储已被虚拟机加载的类信息、常量(常量池)、静态变量、即时编译器编译后的代码、方法表等数据。

  1. 非RAM 存储。若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。

其中两个最主要的例子便是“流式对象”和“固定对象”。对于流式对象,对象会变成字节流,通常会发给
另一台机器。而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对
于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复
成普通的、基于RAM的对象。Java 1.1 提供了对Lightweight persistence 的支持。未来的版本甚至可能提供更完整的方案。

10 总结

Java面向对象的三大主线内容

主线内容 主线分类 备注
Java及其成员 可从属为类的 属性、方法、构造器、内部类
可从属为对象的 构造器、属性、方法、代码块、内部类
OPP的三大特征 封装、继承与多态 多态:声明父类,实例化子类
其他关键字
abstract:抽象类实方
static:静态除类构
final:最终除代构
override:重写仅实方
super/this:
surprise对内调
abstract
全员修饰符
可用于子类继承的类和重写的方法
abstract可以用来修饰:类、方法。abstract天然就是用来重写的。
不可修饰:变量、代码块、构造器、私有方法、静态方法、final方法、final类、接口等。
可以简单理解:抽象类就是比普通类多定义了抽象方法,除了不能直接进行类的实例化操作之外,并没有任何的不同(当然抽象类里面可以没有抽象方法)
final
类员修饰符
final可以用来修饰:类、方法、变量、内部类,均不限定是否为静态。
不可修饰:构造器、代码块.
super
指向对象符
概述:在当前对象里调用父类成员
详细:在当前类的非静态方法、构造器、非静态代码块、内部类中用来调用父类的属性(包静态和非静态)、方法(含静态和非静态)和构造器
this
指向对象符
概述:在当前对象里调用当前类or对象成员(找不到则找父类)
详细:在当前类的非静态方法、构造器、非静态代码块、内部类中用来调用当前类或父类的属性(包静态和非静态)、方法(含静态和非静态)和构造器。默认当前类,当前类找不到再找父类
override
实例/方法修饰符
可用来修饰的地方:当前类的实例方法
static
成员修饰符
static成员修饰符可修饰:属性、方法、代码块、内部类,注意:Java里,static不能修饰类/构造器