JavaSE笔记:面向对象篇

欢迎学习JavaSE,本篇介绍JavaSE面向对象的相关知识,学明白了面向对象编程思想,才算是真正的认识了Java这门语言。本文已完结,但随时可能对内容做出修改,欢迎各位大佬积极斧正,转载请注明出处。

Copyright © 2020-2021 AnonyEast, All rights reserved.

第1-7课内容是我2017年暑假自学Java时总结的编程基础内容,其中删除了与C语言有高度重合的部分,如分支语句、循环语句等。

第1课 Java基本概念

1.环境变量

(1)以键值对的方式存在,一个变量等于一个值,这个值可以是数值、字符、目录等。

(2)操作系统需要运行的最基本的一些变量的值。

2.Path环境变量的作用

(1)Path环境变量是 操作系统外部命令搜索路径

         当在cmd中执行某个命令时,操作系统会将此命令在path环境变量对应的值(目录)中从第一条开始搜索,找到该命令所在的目录后执行该命令。

         eg) 在cmd中输入ipconfig回车,即运行C:\Windows\System32\ipconfig.exe,而path环境变量中存在C:\Windows\这个值,所以该命令可以被搜索到然后执行ipconfig.exe

3.classpath环境变量的作用

(1)classpath环境变量是 类文件搜索路径

         类文件是.class的文件类型。

         classpath环境变量的值为.,.的含义为当前目录

4.JRE和JVM的作用

(1)JRE是Java运行环境,Java程序想要运行必须要有一个基础环境支持(就像厨师离开了厨房就啥用没有),这个环境叫做JRE,

它包括以下几个部分

         *Java虚拟机(JVM):一个由软件虚拟出来的计算机

         *Java平台核心类文件

         *其他支持文件

(2)JVM的作用

         class文件编译完成后,运行该class文件,该class文件会先交给虚拟机(JVM),由虚拟机翻译成适合当前操作系统的代码,然后由操作系统交给硬件去执行。

5.编译器和虚拟机的区别

(1)二者都起到翻译的作用。

(2)编译器是把源文件(.java)翻译成虚拟机可以理解的代码(类文件),虚拟机是把类文件(.class)翻译成操作系统可以理解的代码。需要完成两次翻译程序才能运行。

6.Java运行阶段

输入java a

(1)Java.exe会启动java虚拟机(JVM,JVM会启动类加载器ClassLoader。

     (2)ClassLoader会去硬盘上搜索a.class文件,找到该文件则将该字节码(class)文件装载到JVM中

       (3)JVM将a.class字节码文件解释成二进制数据。

       (4)操作系统执行二进制数据和底层平台进行交互。

7.public class和class的区别

  • 一个java源文件可以定义多个class
  • 一个class会对应生成一个.class字节码文件
  • 每一个class当中都可以设置main方法(程序入口),都可以单独执行。
  • Public不是必须的
  • Public的类名要与java源文件名一致。
  • 一个java源文件只能有一个public类

8.对public static void main(String args[])的解释

这句话是java的程序入口它写在主类里面的
首先为什么是public
因为要在类外面调用main()所以是public
为什么是static
因为系统开始执行一个程序前,并没有创建main()方法所在类的实例对象,它只能通过类名类调用主方法main()作为程序入口,所以该方法是static
为什么是void
因为主方法没有返回值
为什么main
这是主方法名
为什么是String args[]或者String[] args
这表示给主方法传一个字符串数组,字符串名是args

第2课 Java的基本数据类型

1.布尔型变量(boolean)——true or false

(1)boolean类型适用于逻辑运算,一般用于程序流程控制

(2)在java当中的boolean类型只有两种取值可能——truefalse

eg(例):

         boolean b = false

                  b是这个boolean变量的名字,false是这个boolean变量的值。

注意:不能用0和非0,或者空和非空表示boolean型变量。

2.字符型变量(char)——char类型数据用来表示通常意义上的字符

(1)字符是由单引号包括起来的单个字符

         eg)  char c = 'a'

(2)Java字符使用Unicode字符集,正是因为使用Unicode字符集,使得char类型可以存储一个汉字

3)什么是Unicode字符集

说来话长,让我们从ASCll字符集开始说起

补充:ASCll字符集

对字符集的理解可以从以下几个方面入手

(1)在计算机当中,所有数据用二进制数字表示

(2)类似于a、b、c之类的字母无法直接用二进制表示

(3)所以就将所有常见的符号进行编号。标准ASCll码使用7位2进制数表示字符

(4)7位2进制数可以表示所有的数字,大小写字母以及一些常见符号。(例如!,@,#和$等等)

3.Unicode字符集——为每种语言的每个字符设定了统一并且唯一的二进制码

(1)Unicode满足了跨语言文本转换和处理的需求

(2)Unicode在互联网当中扮演非常重要的角色

3Unicode使用数字0-0x10FFFF来表示字符

4)最多允许有1114112个字符

4.整数类型

*超过范围程序会报错

(1)java语言整型常量的三种表示形式

                  ——十进制整数,如12,-314,0

                  ——八进制整数,要求以0开头,如012

                  ——十六进制数,要求以0x0X开头,如0x12

(2)java语言的整型常量默认为int型

                  eg) int i = 3;

(3)声明long型常量可以后加'l''L'

                  eg)  long l = 3L;

5.浮点类型

第3课 变量的赋值

1.字面量

(1)整数字面量为整型(int

(2)小数字面量为双精度浮点型(double

         *不能把大的数值类型赋值给小的数值类型,否则编译会报错

                  eg)  float f = 0.1;

                 此处0.1为双精度浮点型,float为单精度浮点型,不能把大的数值类型赋值给小的数值类型

                 将双精度浮点型变量(字面量)赋值给单精度浮点型,需要在变量(字面量)后加F,否则编译会报错

                  即 float f = 0.1F;

         *进行四则运算时,赋值时使用所有数值中最大的数值类型

                  eg)  double j = 0.5 * 10 - 2 + 7

                  此处0.5为最大的数值类型——双精度浮点型,故赋值时数值类型应该选用double而不是int,否则报错可能会损失精度。

2.强制类型转换(慎用)

(1)在数值前加(数值类型),数值加小括号

         eg)  byte j = (byte)(0.5 * 10 - 2 + 7)

                  此处就是将0.5 * 10 - 2 + 7强制视为byte类型进行运算。

(2)数值型类型表数范围关系

boolean(布尔型) 1字节
byte(整型) 1字节
char (字符型) 2字节
short (整型) 2字节
int (整型) 4字节
float(浮点型) 4字节
long (整型) 8字节
double(浮点型) 8字节

第4课 运算符与表达式

1.算术运算符:+,-,*,%,++,--                   使用数值型类型的变量

         要点一:

                  int i = 3/2

                  请问i的值是多少?

                  答案:i的值是1,因为3和2都是int类型,所以运算结果也会显示为int类型。运算结果显示的数值类型取决于运算数值中的最大数值类型。

                  若要输出1.5,正确写法为

                          double i = 3.0/2 或 double i = 3/2.0 或double i = 3.0/2.0

                  为什么是double?因为如果写成int i = 3.0/2会报错:可能会损失精度。赋值的数值类型必须是最大的数值类型。

         要点二:

                  i++和++i   二者都有将i的值+1的作用

                           i++是先将i的值参与运算,再将i的值+1

                          ++i是先将i的值+1,再参与运算

2.关系运算符: > , < , >= , <= , == , !=  用于boolean型变量

eg)           int i = 5;

                  int j = 6;

                  boolean b = i > j;

                  System.out.println(b);

输出结果为false

3.布尔逻辑运算符的使用

!——逻辑非                                     可以看作是相反数,a为true,!a就为false

&——逻辑与 &&——短路与   除非a与b都为true,才输出true,否则a&b或a&&b 就为false

|——逻辑或 ||——短路或    除非a与b都为false,才输出false,否则a|b或a||b就为true

&&相当于抄近道,有一个为false就直接输出false,不再进行后面的运算。(详情见逻辑与和短路与的区别)

||同理

(1)逻辑与和短路与的区别

逻辑与:eg)

                          int i = 5;

                           boolean e = i > 6 & i++ > 7;

                          输出结果:e为false,i为6

                          原因:先判断i是否大于6,false,然后i的值+1,再判断i++是否大于7,false,则e的值为false

短路与:eg)

                          int i = 5;

                           boolean e = i > 6 & & i++ > 7;

                          输出结果:e为false,i为5

                        原因:先判断i是否大于6,false。只要第一个变量为false就直接输出false。不再判断i++是否大于7,导致i的值没有+1。则e的值为false,i的值为5

4.赋值运算符 = 扩展赋值运算符 += , -=, *=,/=

5.表达式的类型和值

(1)表达式是符合一定语法规则的运算符和操作符的序列

                  i

                  (i+j)-2

(2)表达式的值

        对表达式中操作数进行运算得到的结果称为表达式的值。

(3)表达式的类型

         表达式的值的数据类型即为表达式的类型。

                          eg1)  int i = 5;

                                   int j = i + 5;

                          i + 5的值是10,表达式类型是整型,因为表达式的值是整型。

                          eg2) boolean b = 1 < 10;

                          表达式的值是true,类型是boolean型

                          eg3)j + k - m + 0.5

           该表达式类型是double,因为无论j、k、m值是多少,加上0.5后类型一定是double类型

                          eg4)(m * n) > 100

          该表达式类型是boolean,因为无论m、n的值是多少,最终表达式的值仍然是true或false

第5课 面向对象

从本课开始,正式进入面向对象的学习!

Copyright © 2020-2021 AnonyEast, All rights reserved.

0.先说面向过程

面向过程设计思想 面向对象设计思想
以算法为核心,分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现。 确定该问题由哪些事物组成!先用类模拟出该事物
将大问题转化为若干小问题来求解, 直接面向问题 通过类间接的解决问题
自顶向下设计,要求一开始必须对问题有很深的了解 自下而上设计,从问题的一部分着手,一点一点地构建出整个程序
表现形式:用函数来作为划分程序的基本单位 表现形式:用类来作为划分程序的基本单位
对于“需求不明确、变动较大、规模很大的问题”,显得力不从心 对于需求不明确、变动较大、规模很大的问题非常适合
  对于“需求明确、规模不大、变动较小的问题”则显得十分累赘

(1)面向过程设计的优点

  • 易于掌握与理解,符合人们的思维习惯
  • 对于需求明确、规模不大、变动较小的问题非常适合

(2)面向过程设计的缺点

  • 数据与操作分离开,对数据与操作的修改变得很困难(数据不一定是为函数专门设计的)。
  • 数据的安全性得不到保证(数据不一定需要通过函数操作,可以直接对数据进行操作,不安全)。
  • 程序架构的依赖关系不合理:main函数依赖于子函数,子函数又依赖于更小的子函数;而子函数往往是细节的实现,这些实现是经常变化的,造成的结构就是:程序的核心逻辑依赖于外延的细节,一个细节上的小改动,引起一系列的变动。
  • 对于“需求不明确、变动较大、规模很大的问题”,显得力不从心

1.什么是面向对象

  • 面向对象是一种编程方法

  • 面向对象是一种思维方式

  • 面向对象不是一种编程语言

2.如何学习面向对象?

  • 训练一门面向对象语言的语法

  • 掌握面向对象的思维方式

  • 熟悉面向对象设计原则(开放封闭原则等)

  • 掌握面向对象设计模式

3.面向对象的终极目标:消除程序设计中的重复代码

4.什么是面向对象思维方式?

  • 1.首先确定谁来做,其次确定怎么做

Eg)比如你今天要别人帮你买包泡面,你要先考虑谁去帮你买,在考虑怎么去买。

  • 2.首先考虑整体,其次考虑局部

Eg)比如要用面向对象的思维方式做一个电梯,应先考虑有一个电梯,它有哪些行为(开门、关门、上、下、停),它的额定功率多少,每小时耗电多少等属性……即先考虑整体,而不是先从这个电梯有哪些硬件(电机等)开始考虑。

  • 3.首先考虑抽象,其次考虑具体

Eg)比如你妈让你好好学习,即先有了好好学习,然后才是你该怎么好好学习(去图书馆看书啊、多练习啊等)

5.总结

  • 不要认为掌握了一门面向对象的语言就是掌握了面向对象,关键在于你有没有一颗面向对象的心
  • 习惯于将面向对象与现实世界做比较
  • 记住:所谓的面向对象的程序设计,最终极的目标,就是减少重复代码。

第6课 类(class)

0.什么是类

  • 把一类事物的静态属性和动态可以执行的操作组合在一起。
  • 类是抽象的,用于模拟一类事物,是一个概念。
  • 一旦被定义,类的概念就永远存在了。
  • 不管现实是否存在,比如神啊鬼的,只要需要模拟这个概念,就可以看做一个类

1.定义类的方法

class类名 //有意义的英语单词

{

                  属性;//也叫类数据成员、也叫字段、也叫

                  方法();//也叫类的成员函数,此方法可以直接访问同一个类中的所有属性

}

·属性也叫成员变量,主要用于描述类的状态

         eg)定义Person一个类,则属性为身高体重等,这里的身高也叫类数据成员、也叫字段、也叫

·方法也叫成员方法,主要用于描述类的行为

         eg)Person这个类中,行为有吃饭睡觉等

2.类的表示方法

  • age是类的属性,也叫类成员变量
  • shout是方法也叫类的成员函数
  • shout方法可以直接访问同一个类中的age变量

如果一个方法中有与成员变量同名的局部变量,该方法中对这个变量名的访问是局部变量,而不再是成员变量。

第7课 对象

1.类和对象的关系

  • 类:狗
  • 对象:具体到某一只狗

2.生成对象的方法

格式:类名对象名 = new类名();

         eg) Dog dog = new Dog();

                  Dog是一个类(Dog.class),dog是对象名(可以看做一个变量)。

(1) Dog dog = new Dog();

         创建一个dog的引用(也就是C中的指针)在栈中,即dog这个引用自身所占的内存在栈中分配,且dog指向堆中新生成的对象(保存新生成的对象的地址)。

(2) Dog dog = new Dog();

         创建一个Dog的对象在堆内存,堆内存用于存储新生成的对象(动态分配),见到new就往堆内存中生成新对象

(3)Dog dog = new Dog();

         将新创建的Dog对象赋给这个引用dog。用C语言理解就是:

Dog *dog = (Dog*)malloc(sizeof(Dog));//假设这里的Dog是一个数据类型,类似于结构体

3.对象的使用方法

         先生成一个对象,Dog d = new Dog();Dog是一个类,d是一个对象。

  • 对象.变量:对该对象的变量进行赋值

d.name = “旺财”;//d的名字是旺财

  • 对象.函数():调用该对象的函数(方法)

d.jump();//通知d这只狗开始蹦跳

4.生成多个对象

class TwoDog{
   pubilc static void main(String args[]){
      Dog d1 = new Dog();//生成一个对象d1
      Dog d2 = new Dog();//生成一个对象d2
      d1.name = "大毛";
      d2.name = "二毛";
      d1.color = "白色";
      d2.color = "黑色";
      d1.jump();
      d2.jump();
   }
}

5.匿名对象的使用

         匿名对象:可以用不定义对象的引用名称,而直接调用这个对象的方法,这样的对象叫做匿名对象。

                          eg) new Dog().jump();

                                   匿名对象只能用一次,不能重复使用,再也找不到它。

6.对象调用方法的JVM内存分配

(1)两个对象调用同一个方法(若看不清图片请放大网页或将图片右键在新标签页打开)

(2)两个引用指向同一个对象

(3)使用对象类型作为方法的参数

(4)使用对象类型作为方法的返回值

第8课 访问控制符

一、访问控制符的类型


*前言:在一个类的内部,所有的成员可以相互访问,访问控制符是透明的;访问控制符是针对外部访问而言的。

*外部访问包括:(1)通过类名访问该类内部的成员。

(2)通过类对象名访问该类内部成员


1. 公有访问控制符(public):可以通过外部访问方式访问内部的public成员

2. 私有访问控制符(private): 不可以通过外部访问方式访问内部的private成员, 只能被该类自身所访问和修改 。

3. 保护访问控制符(protected): 可以被该类自身与它在同一个包中的其他类在其他包中该类的子类 访问。

4. 默认访问控制符: 只能被同一个包中的类访问和引用 。

class A{
    private int i;
    public int j;
    protected int k;
}
class TestAccess{
    public static void main(String[] args){
        A aa = new A();
        aa.i = 10;//不行,因为i是私有的
        aa.j = 20;//可以,因为j是公共的
        aa.k = 30;//可以,因为k是保护的
    }
}

5.总结一下

public:任何地方均可访问

protected:同一包和子类可见

默认:同一包中可见

private:仅该类内部可见

二、一些建议

编写代码时,如果没有特殊的考虑,建议这样使用权限:

1.成员变量使用 private ,隐藏细节。

2.构造方法使用 public ,方便创建对象。

3.成员方法使用 public ,方便调用方法。

三、定义类时的权限修饰符规则

外部类:public / (default)

成员内部类:public / protected / (default) / private

局部内部类:什么都不能写(此处不能理解成default)

第9课 函数的重载

一、函数的重载

1.同名的函数通过不同的形参做类似的事情。

2.函数重载的要求

  • 函数的形参个数
  • 函数的形参顺序
  • 函数的形参数据类型
  • 这三个至少有一个是不一样的

3.如果两个函数只是函数的返回值不一样,其他都一样,这构不成函数的重载,并且编译时会报错!

第10课 类的构造函数

一、构造函数

1.特点

  • 函数(方法)名与类名是相同的
  • 不能有返回值(返回值为void的函数也不是构造函数)
  • 可以有参数,也可以没有参数
  • 可以有多个

2.说明

  • 声明一个对象,系统首先为该对象分配内存,然后立即自动调用该对象的构造函数

3.注意

  • 任何一个类对象被生成时一定会调用该类的构造函数
  • 无论一个类有多少个构造函数,生成一个类对象时一定只会调用其中的某一个构造函数!
  • 如果一个类定义了其他的构造函数,则生成对象时不再调用默认的构造函数。

class A
{
	private int i;
	private int j;
        //无参构造函数
	public A()
	{
		System.out.printf("无参构造函数被调用了!\n");
	}
	//有参构造函数		
	public A(int a, int b)
	{
		i = a;
		j = b;
		System.out.printf("有参构造函数被调用了!\n");
	}
	public void show()
	{
		System.out.printf("i = %d, j = %d\n", i, j);
	}
}
class TestConst_2
{
    public static void main(String[] args)
    {
	A aa = new A(1, 2);//先生成一个对象aa,然后立即将参数1和2传入classA中的有参构造函数,此时i,j被赋值
	//A aa2 = new A(1,2,3);  //error
	A aa3 = new A();//先生成一个对象aa,然后立即将参数1和2传入classA中的无参构造函数
        aa.show();
    }
}

二、构造函数的返回值问题

//函数名与类名相同的函数用法
class A {
    private int i;
    //这不是构造函数,创建类对象时是
    //不会自动被调用的,与16行的A函数构成重载
    public void A(int m) // 6行
    {
        i = 11111;
        System.out.println("调用了有参的函数");
    }
    //将本函数注释掉,则输出结果是i = 0,
    //如果本函数不被注释掉,则输出结果是:i = 10
    public A()
    {
        this.i = 10;
    }
    // 这不是构造函数,创建类对象时是
    //不会自动被调用的,与6行的A函数构成重载
    public int A() // 16行
    {
        i = 22222;
        System.out.println("调用了无参的函数");
        return 888;
    }
    public void show() {
        System.out.println("i = " + i);
    }
}
class TestConst_1 {
    public static void main(String[] args) {
        A ra = new A();
        ra.show();
        ra.A();
        ra.show();
        ra.A(1);
        ra.show();
    }
}
//输出结果: 
//i = 10 调用了无参的函数 i = 22222 调用了有参的函数 i=11111

总结:在一个类中可以定义多个函数名与类名相同但却有返回值的函数, 返回值为void 或int 或double都可以,这些有返回值的函数只要能满足重载特点,就可以同时存在一个类中
不过要注意:这些有返回值的函数(包括返回值为void的函数)都不是构造函数,都不会被类对象自动调用。当然也可以定义多个没有任何返回值的函数,注意连void都不可以加, 这些函数才是构造函数。 在构造方法中不能使用return语句返回一个值,但是可以单独使用return作为方法的结束 。

三、构造函数数据成员的赋值问题

  • 一个类中的数据成员
  • 1、 如果在定义的时候不初始化,则它的值是系统自动分配好的默认值! 如int型为零,boolean型是false。 如本程序的A类对象就是这样
  • 2、 如果在定义的同时赋初值, 则是可以的,也就是说该值是生效的。 如本程序的A类对象就是这样
  • 3、 如果在定义的同时赋初值,当然生效,但如果在构造函数中又改变了定义时赋的初值,则该数据成员最终的值就是构造函数中修改之后的那个值,因为:系统会先执行定义时赋的初值,然后再执行构造函数中赋的初值。 如本程序的B类对象就是这样

class A {
    int i;
    int j = 10;
    boolean flag;
    void show() {
        System.out.println("i = " + i);
        System.out.println("j = " + j);
        System.out.println("flag = " + flag);
    }
}
class B {
    int i;
    int j = 10;
    boolean flag;
    public B() {
        System.out.println("以前的值是 " + i + "  " + j + " " + flag);
        i = 88;
        j = 88;
        flag = true;
    }

    void show() {
        System.out.println("i = " + i);
        System.out.println("j = " + j);
        System.out.println("flag = " + flag);
    }
}
class TestConst_3 {
    public static void main(String[] args) {
        A aa1 = new A();
        aa1.show();
        B bb1 = new B();
        bb1.show();
    }
}
/*输出结果:
i = 0
j = 10
flag = false
以前的值是 0 10 false
i = 88
j = 88
flag = true
*/

四、多个构造函数可能带来的冲突

注释掉6-8行代码就会报错

五、构造代码块

{//构造代码块    
}

  1. 构造代码块的作用是给对象进行初始化。
  2. 对象一建立就运行构造代码块了,而且优先于构造函数执行。这里要强调一下,有对象建立,才会运行构造代码块,类不能调用构造代码块的,而且构造代码块与构造函数的执行顺序是前者先于后者执行。
  3. 构造代码块与构造函数的区别是:构造代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。

第11课 this关键字

一、this关键字(this指针)

class A{
  public int i;
  public A(int j){
    i = j;
  }
  //show方法参数列表中有一个隐藏的this指针指向当前调用该方法的对象
  public void show(){
    System.out.println("i = " + i);
  } 
}
class TestThis_1{
  public static void main(String[] args){
    A aa1 = new A(5);
    A aa2 = new A(8);
    aa1.show();//aa1调用show,this指针指向aa1,输出i=5
    aa2.show();//aa2调用show,this指针指向aa2,输出i=8
  }
}

二、this关键字的应用

1.使用this关键字引用成员变量

class A{
  private int i;
  public A(int i)//这里用i不用j是因为懒得想一个新的变量名
  {
    //this关键字访问成员变量
    this.i = i;  
/*将形参 i 赋给该构造方法本次运行
所创建的那个新对象的i数据成员*/
  }
  public void show(){
  System.out.println("i = " + this.i);
  //this表示当前时刻正在调用show方法的对象
  //this可以省略
  }
}
public class TestThis{
  public static void main(String[] args){
  A aa1 = new A(100);
  aa1.show();
  A aa2 = new A(200);
  aa2.show();
  }	
}

2.使用this关键字调用成员方法

以下代码通过this指针让类中一个方法,访问该类的另一个方法或实例变量。Dog类中run()方法调用jump()方法一定要通过对象调用,因为jump()方法并没有被static修饰。 但不必在run()方法中重新定义一个Dog类,因为DogTest类中当程序调用run()方法时,一定会提供一个Dog对象,这样就可以直接使用这个已经存在的Dog对象,而无须创建新的Dog对象。
因此通过this关键字就可以在run()方法中调用该方法的对象。

class Dog {
    public void jump() {
        System.out.println("正在执行jump方法");
    }
    public void run(){
        //Java允许省略this前缀,直接写成jump();
        this.jump();
        System.out.println("正在执行run方法");
    }
}

public class DogTest {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.run();
    }
}
//输出结果:
//正在执行jump方法
//正在执行run方法

第12课 static(静态)关键字

一、static属性

1.静态成员属于类本身的,而不是属于对象,被类的所有对象所共有

2. 即便不创建对象,也可以使用类本身的静态成员

3.静态成员分为:静态数据成员静态方法成员

  • 看清楚是成员哦,所以static不能用在局部变量中!

4.使用静态成员的两种方法

  • 类名.静态成员名
  • 类对象名.静态成员名

二、 static属性的特点及注意事项

1.同一个类的多个对象,共用一个具有static属性的成员(成员:数据或方法)

2.具有static属性的成员,可以通过类名直接访问

3.static属性的成员虽然属于类本身,但仍然可以通过对象来访问

4.一个static成员能不能直接通过类名访问,还需满足这个成员不是private

5.同一个类中,静态方法不能访问非静态成员,非静态方法可以访问类中的所有成员(包括静态成员)

6.静态方法不能以任何方式引用this和super关键字(super关键字在下一章讲解)。静态方法在使用前不用创建任何实例对象,当静态方法被调用时,this所引用的对象根本就没有产生。

class A {
    public static int i;
    public int j;
    public static void f() {
        // g(); //error 静态方法不能访问非静态方法
        i = 10; // OK
        // j = 99; //error 静态方法不能访问非静态属性
    }
    public void g() {
        f(); // OK 非静态方法可以访问类中的所有成员
        i = 20; // OK
    }
}

补充知识:为什么静态方法无法调用非静态成员?

  • 静态方法是属于类的,非静态方法属于实例对象。在一个类被调用的时候会进行类加载,类加载会分配内存,可以通过类名直接去访问;非静态成员属于类的对象,所以只有在对象实例化(生成对象)之后才分配内存,然后通过类的对象去访问
  • 因此,当静态方法调用非静态成员,如果此时类中并没有实例化的对象,则对象在内存中不存在,那么这个非静态成员在内存中也不存在, 你怎么能访问一个内存中不存在的东西呢?

三、static的应用举例

1. 统计一个类产生的实例对象的个数

以下代码是求三角形的周长和面积,综合了面向对象编程思想、访问控制符、构造函数、this指针和static的知识,建议好好看看。

class triangle{//定义一个类,叫做三角形
    /*静态成员count属于类,用于统计三角形个数
      设置private使变量只能在这个类的内部被访问*/
    private static int count = 0;
    private int a,b,c;
    triangle(int a,int b,int c){//类的构造函数
        count++;//此处的count是被triangle类中的所有实例对象共用的
        this.a=a;//this指向当前调用triangle函数的对象
        this.b=b;
        this.c=c;
    }
    int circumference(){//周长方法
        return a+b+c;
    }
    double area(){//面积方法
        double p=(a+b+c)/2;
        return Math.sqrt(p*(p-a)*(p-b)*(p-c));
    }
/*show方法设置成static后
  可以通过类名直接访问*/
    static void show(){
        System.out.println("三角形(对象)个数 = "+count);
    }
}
public class triangleCalc{//定义一个公共类,叫做三角形计算
    public static void main(String args[]){//程序入口
        triangle n1=new triangle(3,4,5);//传入三边长
        triangle n2=new triangle(5,12,13);
        System.out.println("周长= "+ n1.circumference() + ",面积= "+n1.area());
        System.out.println("周长= "+ n2.circumference() + ",面积= "+n2.area());
        triangle.show();//打印对象个数,直接通过类名访问
    }
}

2.要求一个类只生成一个实例对象

以下代码实现了class A中永远只有一个对象,且无法通过外部方法new出class A的新对象,提高了程序的安全性。

//8行的static不能省,否则10行无法访问对象obj
//9行的static不能省,否则18行无法通过类名调用getObj()
class A{
    int i;
    private A(){
    //本函数用于禁止在其他类中通过new创建A类的对象
    }
    private static A obj=new A();//8行,此时对象obj成为A类的一个属性
    static A getObj(){//9行
        return obj;//10行
    }
    void show(){
        System.out.println("i = "+i);
    }
}
public class staticApply_2{
    public static void main(String[] args) {
        A obj1=A.getObj();//18行
        A obj2=A.getObj();
        //A obj3=new A();//error A()是private,无法访问
        obj1.i=233;
        obj2.show();
        //输出结果:i = 233,
        //说明obj1和obj2共用同一段内存
    }
}

四、静态代码块

static {//静态代码块    
}

  1. 静态代码块是随着类的加载而执行,只执行一次,并优先于主函数。具体说,静态代码块是由类调用的。类调用时,先执行静态代码块,然后才执行主函数的。
  2. 静态代码块其实就是给类初始化的,而构造代码块是给对象初始化的。
  3. 静态代码块中的变量是局部变量,与普通函数中的局部变量性质没有区别。
  4. 一个类中可以有多个静态代码块

五、变量的类型总结

(1)成员变量

定义在类中方法外,当类被实例化时,成员变量就会在内存中分配空间被初始化,直到这个实例化对象的生命周期结束时,成员变量的生命周期才结束。所以,类的成员变量的作用范围与类的实例化对象的作用范围相同。

(2)静态变量

由static关键字修饰的成员变量被称为静态变量。静态变量不依赖于特定的实例,而是被所有的实例共享。只要一个类被加载,JVM就会给类的静态变量分配存储空间,因此,可以直接通过类名和变量名来访问静态变量

(3)局部变量

局部变量的作用域与可见性为它所在的花括号内。如方法中定义的变量,就是局部变量

(4)从内存分配的角度来看,

成员变量的数据存储于堆中该成员变量所属的对象里面,堆内存中存放的变量都会进行默认初始化

局部变量(包括基本数据类型和对象引用) 存放在栈内存中,局部变量的大小是可以被确定的;局部变量会在其自身所属方法(或代码块)执行完毕后,被自动释放。所以局部变量的生命周期也是可以被确定的。栈内存中存放的局部变量不会进行默认初始化。 所以,我们在声明一个成员变量时,可以不用对其进行初始化赋值。而如果声明一个局部变量却未进行初始赋值,如果想对其进行使用就会报编译异常。

静态变量存储在方法区中,在类被加载时,JVM就会给为其分配存储空间。

第13课 继承(extends)

一、继承

1.一个新类从已有的类那里获得其已有的属性和方法,这中现象叫类的继承

2.这个新类被称为子类,也叫派生类已有的那个类叫做父类,也叫做基类

3.继承的好处

  • 代码得到极大的重用
  • 形成一种类的层次体系结构
  • 为多态创造条件

二、继承的实现方式

extends关键字:class SubClass extends SuperClass{}

SubClass是子类(派生类),SuperClass是父类(基类),SubClass 继承 SuperClass 的属性。

class Human{
     //人笑
     public void laugh(){
         System.out.println("笑!");
     }
     //人哭 
     public void cry() {     
         System.out.println("哭!"); 
    }
 }
//类Student继承类Human
 class Student extends Human{
     //学生做作业
     public void doHomework(){
         System.out.println("做作业!");
     }
 }
 public class TestExtends_2{
     public static void main(String[] args){
         //new一个Student的对象s
         Student s = new Student();
         /*哭和笑本来是Human的属性,
         却能成功调用,说明Human被Student继承*/
         s.laugh();
         s.cry();
         s.doHomework();
     }
 }
 /*在JDK 1.6中的运行结果是:
 笑!哭!做作业!*/

三、同包继承权限问题(重点)

温馨提示:虽然私有(private)成员不能被继承,但实际上私有成员物理上已经被继承过来只不过逻辑上程序员不能去访问它。因此继承必须慎重,否则会浪费内存

以下代码证明了:

  • 子类内部可以访问父类非私有的成员
  • 私有成员无法被子类方法访问
  • 通过子类对象名只能访问从父类继承过来的非私有成员

class A{
    public int i;
    protected int j;
    private int k;
    public void g() { }
    private void s() { } 
    protected void b() { }
 }
 class B extends A{
    private void m(){
         i = 10;
         j = 20;
         //k = 30; //error 私有属性不能被继承
        g();
        b();
        //s(); // error 私有方法不能被继承
     }
    public void f() {     
        i = 10;     
        j = 20;     
        //k = 30; //error 私有属性不能被继承
        g();
        b();
        //s(); // error 私有方法不能被继承 
    }
}
 class M{
     public static void main(String[] args){
         B bb = new B();
         bb.i = 20;
         bb.j = 30;
         bb.b();
         bb.g();
        //bb.s(); //error 私有方法不能被继承
        //bb.k = 22; 私有属性不能被继承
     }
}

四、何时选择继承

  • 一个很好的经验:“B是一个A吗?
  • 如果能说B是一个A,则B可以是A的一个子类。
  • 例如人是一个类,学生是一个类,学生是人,所以学生可以是人的子类。

  • 常犯的错误:“A有一个B吗?”
  • 例如汽车有一个发动机,发动机是汽车的子类吗?
  • 不是!因为发动机不是一个汽车。

五、Java只支持单继承

六、子类访问父类成员的三种方式

  • 子类内部访问父类成员
  • 通过子类对象名访问父类成员
  • 通过子类的类名访问父类成员(此时需要父类成员是static)

class A{
    public static int i;
    protected static int j;
    private static int k;
}
class B extends A{//类B继承了类A
    private void fun(){
        i=2333;//在子类内部访问父类成员
        //k=666;//error 私有的无法访问
    }
}
public class testExtends_2{
    public static void main(String[] args) {
        A.i=99;//OK
        B.i=99;//通过子类的类名访问父类成员
        A.k=666;B.k=666//error 私有的无法访问
        B obj1=new B();
        obj1.i=12345;//通过子类对象名访问父类成员
    }
}

第14课 super关键字

一、 super是什么?

java中的super关键字是一个引用变量,用于引用父类对象。关键字“super”以继承的概念出现在类中。 super用于成员方法和构造方法中,不能用于静态方法中。

二、super的使用

1.使用super访问父类的变量

当父类和子类具有相同的数据成员时,会发生此情况。 用法为“super.变量名”。

以下代码父类和子类都有一个成员maxSpeed。我们可以使用super实现在子类中访问父类的maxSpeed。

/* 父类Vehicle */
class Vehicle
{
    int maxSpeed = 120;
}
/* 子类Car */
class Car extends Vehicle
{
    int maxSpeed = 180;
    void display()
    {
        /* 打印父类Vehicle的成员maxSpeed */
        System.out.println("Maximum Speed: " + super.maxSpeed);
    }
}
class testSuper_1
{
    public static void main(String[] args)
    {
        Car small = new Car();
        small.display();
    }
}
//输出结果:
//Maximum Speed: 120

2. 使用super调用父类方法

当父类和子类具有相同的命名方法 , 使用super关键字来调用父类的方法。

以下代码,如果我们只调用方法message(),那么当前类的message()被调用,但是使用super关键字时,父类的message()也可以被调用。

/* 父类Person */
class Person
{
    void message()
    {
        System.out.println("This is person class");
    }
}
/* 子类Student */
class Student extends Person
{
    void message()
    {
        System.out.println("This is student class");
    }
    // Note that display() is only in Student class
    void display()
    {
        message();//当前类的message()被调用
        super.message();//父类的message()被调用
    }
}
class testSuper_2
{
    public static void main(String args[])
    {
        Student s = new Student();
        s.display();
    }
}
//输出结果:
//This is student class
//This is person class

3. 在子类的构造函数中使用super调用父类的构造函数

以下代码,在子类构造函数中使用super关键字调用父类构造函数 普通方法和static方法中不能调用父类的构造方法。

class A
 {
     A()//无参构造方法A()
     {
         System.out.println("AAAAA");
     }
    A(int i) { }//有参构造方法A(int)
 }
 class B extends A//B继承A
 {
     B()//
     {
         super(2); 
        //如果把该语句注释掉的话,则编译器默认的是自动调用super()
        //但如果父类没有无参的构造函数,则会报错
        //一个子类的构造函数中只能出现一个 super(...)
         System.out.println("BBBBB");
     }
 }
 class C extends B
 {
     C() 
     {
         //int k = 10; 
         //如果上述语句生效,则会出错,
         //因为会导致super()语句不是此构造函数的第一条语句
          super();  //35行
//每个子类构造方法的第一条语句,都是调用super(),
//如果父类没有无参的构造函数,那么在编译的时候就会报错。
//super();语句可以写,也可以不写,不写的化,
//系统会自动调用无参数的super()          
//如果写出来的话,必须保证是第一条语句,否则报错              
//super()也可以改为super(2); 但前提是父类必须
//有带一个参数的构造函数,否则也会报错
//如果把35行改为 super(2); 编译时就会报错。    
          System.out.println("CCCCC"); 
      }
 }
 class  TestSuper_1
 {
     public static void main(String[] args) 
     {
         C cc = new C();
     }
 }
 //输出结果:
 //BBBBB
 //CCCCC

子类构造函数中使用super关键字调用父类构造函数 的总结:
1、每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。

2、如果写出super();语句,则必须保证该语句是第一条语句,否则会出错
3、super();如果不写,则编译器会自动添加,所以此时如果父类没有无参的构造函数就会出错
4、既可以写出super();前提是父类必须有无参的构造函数。
也可以写出super(实参); 前提是父类必须有带参数的构造函数。
5、调用父类的构造函数的语句必须借助于super,不能直接写父类的类名。

第15课 父类方法重写

一、方法重写

1.指在子类中重新定义父类中已有的方法

2.重写方法必须和被重写方法具有相同的方法名称、参数列表和返回值类型

同名同参同返回值

3.子类中不允许出现与父类同名同参但不同返回类型的方法,如果出现了,编译时会报错。因为如果返回类型不同,相当于没有重写,而是写了一个新的同名方法,这会导致编译器不知道调用哪一个方法。

4.覆盖方法时,不能使用比父类中被覆盖的方法更严格的访问权限(原因多态时再讲)。也就是子类重写方法的权限要高于父类的同名方法。若子类方法为protected,父类方法为public,就会报错。

二、方法重写示例

例如鸟类都包含了飞翔方法,其中有一种鸟是特殊鸟类——鸵鸟,因此它也会从鸟类获得飞翔方法,但这个飞翔方法明显不适合鸵鸟,所以,鸵鸟需要重写鸟类的方法。

class Bird {
    //Bird 类的fly()方法
    public void fly()
    {
        System.out.println("我在天空可劲的飞啊");
    }
}
class Ostrich extends Bird{
    // 重写Bird类的fly()方法
    public void fly() {
        super.fly();//super可以访问父类被隐藏的实例变量
        //注释掉上一行就不能打印出"我在天空可劲的飞啊"
        System.out.println("我能在地上可劲跑啊");
    }
public static void main(String[] args) {
        // 创建Ostrich对象
        Ostrich os = new Ostrich();
        // 执行Ostrich对象的fly()方法
        os.fly();
    }
}
//输出结果:
//我在天空可劲的飞啊
//我能在地上可劲跑啊

第16课 多态、 instanceof 关键字

多态第一次学习很难彻底理解,随着学习的深入对多态的理解会慢慢加深。第一次学,先把语法掌握好,至于多态的思想能掌握多少算多少,因为后期的学习还会有要用到多态的地方。

一、多态

1.一个父类的引用类型变量它既可以指向父类对象也可以指向子类对象,它可以根据当前时刻指向的不同,自动调用不同对象的方法,这就是多态。

达到的效果: 同一个接口,使用不同的实例而执行不同操作 。

2.前提: (1) 继承或者实现【二选一】

(2) 方法的重写【意义体现:不重写,无意义】

(3) 父类引用指向子类对象【格式体现】

以下代码中,aa可以根据它自己当前时刻指向的是A类对象还是A子类对象,而自动决定调用哪个对象的f方法,这就是多态。

class A
 {
     public void f()
     {
         System.out.println("AAAA");
     }   
 }
 class B extends A
 {
     public void f()
     {
         System.out.println("BBBB");
     }
 }
 public class TestPoly_1
 {
     public static void main(String[] args)
     {
         A aa = new B();
         aa.f();//输出BBBB
         aa = new A();     
         aa.f(); //输出AAAA

         B bb1 = new B();
         bb1.f();//输出BBBB
         A aa1 = new A();
         aa1 = bb1;//OK 把子类的引用发送给父类
         aa1.f();//输出BBBB
         //bb1 = aa1;//error 不能把父类的引用传给子类
     }                                   
 }

3.当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误如果有,执行的是子类重写后的方法

4.多态访问成员变量的两种方式

(1)直接通过对象名称访问成员变量:等号左边是谁,优先用谁,没有则向上找,找不到就报错。

(2)间接通过成员方法访问成员变量(如getXXX方法):看该方法属于谁(哪个对象),优先用谁,没有则向上找

5.多态访问成员方法

(1)看new的是谁,就优先用谁,没有就向上找

即:编译看左边,运行看右边。左边是父类,父类没有该方法,就编译报错。右边是子类,运行方法时调用的是子类重写后的方法。

二、多态的优点

以下代码,实现了通过一个函数就可以实现调用整个A类族所有对象的f方法的功能。

/*假设A派生出B  B派生出C
编程实现调用A  B  C类对象f方法的函数*/
 class A
 {
     public void f()
     {
         System.out.println("AAAA");
     }
 }
 class B extends A
 {
     public void f()
     {
         System.out.println("BBBB");
     }
 }
 class C extends B
 {
     public void f()
     {
         System.out.println("CCCC");
     }
 }
 public class TestPoly_1
 {
     //m函数可以实现调用整个A类族所有对象f方法的功能
     //A是数据类型 aa是形参(引用类型,相当于C的指针) 
     public static void m(A aa)
     {
         aa.f();//调用传入的对象所对应类的f()函数
     }
     public static void main(String[] args) {
         A aa = new A();
         B bb = new B();
         C cc = new C();
         m(aa);
         m(bb);
         m(cc);
      }
 }

三、多态的类型转换(难点)

1.对象的向上转型

安全,但有弊端:无法访问子类特有的成员。

class A{
     public void f(){
         System.out.println("AAAA");
     }
 }
 class B extends A{
     public void f(){
         System.out.println("BBBB");
     }
     public void g() {     
         System.out.println("哈哈"); 
     }
 }
 public class TestPoly_4{
     public static void main(String[] args){
         A aa = new B();//等价于 A aa;aa = new B();
         aa.f();//aa指向B的对象
         //aa.g();  //error 不能访问子类特有的成员
     }
 }
//输出结果:
//BBBB

2.对象的向下转型

格式:子类名称 对象名 = (子类名称)父类对象

将父类对象还原成本来的子类对象

 class A{}
 class B extends A{}
 public class TestPoly_3
 {
     public static void main(String[] args)
     {
         A aa = new A();
         B bb = new B();
         //bb = aa; //error 类型不一致
         //bb = (B)aa;  //24行   
//24行编译没有错误,但运行时出错!因为aa指向的是父类对象
         A aa2 = new B();//多态:aa2指向B类对象
         //bb = aa2;//error 永远不可以把父类引用直接赋给子类引用
         bb = (B)aa2;  //28行
//28行OK 因为aa2,本身指向的就是一个B类对象,
//所以可以进行强制转化,注意与24行的区别。
//如果父类引用指向的是个子类对象,
//则可以通过强制类型转化把父类引用强制转化为子类引用,
//注意必须强制转化,
//在Java中无论如何绝对不可能直接把父类引用赋给子类引用的 
     }
 }

四、使用 instanceof 关键字

为了避免ClassCastException(类转换异常)的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,格式如下:

变量名 instanceof 数据类型

如果变量属于该数据类型,返回true。 如果变量不属于该数据类型,返回false。

public class Demo02Instanceof {
    public static void main(String[] args) {
        Animal animal = new Dog(); // 本来是一只狗
        animal.eat(); // 狗吃SHIT
        // 如果希望掉用子类特有方法,需要向下转型
        // 判断一下父类引用animal本来是不是Dog
        if (animal instanceof Dog) {
            Dog dog = (Dog) animal;
            dog.watchHouse();
        }
        // 判断一下animal本来是不是Cat
        if (animal instanceof Cat) {
            Cat cat = (Cat) animal;
            cat.catchMouse();
        }
        giveMeAPet(new Dog());
    }
    public static void giveMeAPet(Animal animal) {
    //判断传入的动物是猫还是狗
        if (animal instanceof Dog) {
            Dog dog = (Dog) animal;
            dog.watchHouse();
        }
        if (animal instanceof Cat) {
            Cat cat = (Cat) animal;
            cat.catchMouse();
        }
    }
}

第17课 抽象类

一、为什么需要抽象类

1.抽象类的由来

  • 利用抽象类是为了更好的对类加以分类,就如同人类不但给各种具体植物取了名字还发明了“植物”这个抽象的词对所有具体植物进行归类一样

2.Java用来模拟现实世界,所以也存在抽象类

3.抽象类通常用来作为一个类族的最项层的父类,用最底层的类表示现实中的具体事物,用最顶层的类表示该类族所有事物的共性

二、抽象方法

1.在定义Java方法时可以只给出方法头,而不给出方法内部实现代码(没有花括号),这样的方法称为抽象方法。

2.凡是没有方法体的方法必须的使用关键字abstract修饰为抽象方法

3.凡是含有抽象方法的类都必须的声明为抽象类

三、抽象类

1.用abstract关键字来修饰一个类时, 该类叫做抽象类

2.包含抽象方法的类必须声明为抽象类

 /*如果f方法没有方法体,
则必须的在class前加abstract */
abstract class  A  {
     private int i;
 /*如果f方法没有方法体,则f方法
必须声明为抽象方法,即必须的在前面加abstract*/
     public abstract void f();  
 }

3.但是一个抽象类中却可以不包含任何抽象方法,尽管比较少见

4.抽象类不一定有抽象方法,有抽象方法的一定是抽象类

5.不能new出抽象类对象,但可以定义一个抽象类的引用

//假设A是一个抽象类
//不能new出抽象类对象
A obj1 = new A();//error
//可以定义一个抽象类的引用
A obj2;//OK

四、抽象类实现多态的示例

我们可以把一个子类对象的地址赋给抽象类的引用,然后通过抽象类的引用调用子类从父类继承过来的方法,即抽象类也可以实现多态

  • 以下代码中A是抽象类,B是A的子类且完全实现了A的所有抽象方法,用抽象类的引用访问子类的方法,这就是多态

abstract class A{
     abstract public void f();
 }
 class B extends A{
     public void f(){
         System.out.println("BBBB");
     }
 }
public class TestAbstractClass{
     public static void main(String[] args){
         //不能new出抽象类对象 
         //A aa = new A();  //error  
         //可以定义一个抽象类的引用 
         A aa;  
         //将子类对象的地址赋给抽象类的引用
         A aa = new B();  //OK
         //多态:用抽象类的引用访问子类的方法 
         aa.f();  
     }
 }
//输出结果:BBBB

第18课 final关键字

一、final的特性

  • final修饰的整个类不能被继承
  • final修饰的类中的属性是常量,只能赋值一次
  • final修饰的类中的若干个方法不能被子类重写
  • 方法内部的属性若需要修饰,只能用final,不允许public、static等,否则编译阶段报错

二、final修饰整个类

1.表示该类不能被继承

2.如果认为一个类已经很完美且不需要定义子类来继承它时,可以使用它

3.格式:

//public和final可以互换
public final class A{...}

三、final修饰类中的若干属性

1.final修饰类中的若干属性表示该属性必须被赋值并且只能被赋一次值(注意默认值不算真正的赋值),相当于一个常量如果是引用类型的变量被final修饰, 那么该变量存的是一个内存地址,该地址就不能变了,但是该内存地址所指向的那个对象还是可以变的,就像你记住了人家的门牌号,但你不能管人家家里人员数量,。

2.初始化方式有两种:(只能选择其中的一种)

  • 在定义成员变量的同时初始化
  • 如果没有在定义的同时初始化,则必须在该类中的所有构造函数中完成初始化

3.注意:一个类的所有普通方法内部都不可以修改final修饰过的成员变量的值

class Point {
    //构造函数被调用时int成员变量的值默认初始化为0
    int x;
    //final定义的变量在定义的同时初始化
    final double PI = 1.9; //10行 OK
    Point() {
        //只要10行对PI进行了初始化,以下语句
        //就必须的被注释掉,否则编译时会报错!
        // PI = 1.234; 
    }
    Point(int m, int n) {
        //只要10行对PI进行了初始化,以下语句
        //就必须的被注释掉,否则编译时会报错!
        // PI = 2.33; 
    }
    void output() {
        //一个类的所有方法内部都
        //不可以修改final修饰过的成员变量
        // PI = 2; //error 
        System.out.printf("%d %f\n", x, PI);
    }
    public static void main(String[] args) {
        Point pt = new Point();
        pt.output();
    }
}
//输出结果:0 1.900000

四、final修饰类中的若干方法

被final修饰的方法可以被子类继承,但不能被子类重写

class A{
     //如果在public前面加final,则编译时就会报错 
     public void f(){
         System.out.println("AAAA");
     }
 }
 class B extends A{
     //重写父类方法
     public void f(){
         System.out.println("BBBB");
     }   
 }
 public class TestFinal{
     public static void main(String[] args){}
 }

第19课 接口(interface)

一、接口

1.接口的定义:抽象方法和常量值的集合。接口就是多个类的公共规范,接口是一种引用数据类型。

2.接口的格式

[修饰符] interface interfaceName [extends SuperInterfaceList]{
    [public][static][final] 常量类型 常量名 = 常量值;
    [public][abstract] 方法返回值类型 方法名(参数列表);
}

二、接口的语法规则

1.JDK6中:接口中定义的属性必须是public static final的,而接口中定义的方法则必须是public abstract的,因此这些修饰符可以部分或全部省略

即:属性是常量,方法是抽象。

但是从JDK8开始,接口中允许有默认方法(default)和静态方法(static),JDK9还支持了私有方法。static方法可以通过"接口名. 方法名"来调用,而非静态方法则需要实例化的对象来调用。因此在接口中定义抽象方法时,可以省略 public abstract 修饰符,定义default或static方法时,可以省略 public 修饰符(default不可省略)。接口中的属性仍然必须是 public static final修饰。

2.接口中定义的属性的值在实现类(实现该接口的类)中不能被更改

//接口中是不可以定义构造函数的
 interface It{
     //不能改为 int i; 
     int i = 10; 
 }
//类A实现(implements)It接口
 class A implements It{
     public A(int j){
         //接口It中的属性i是public static final类型,
         //不可以在实现类中被改变 
         //this.i = j;  
     }
 }
 class TestInter_1{
     public static void main(String[] args){
         A aa = new A(10);
         System.out.println("aa.i = " + aa.i);
         System.out.println("aa.i = " + A.i);
     }
 }

3.一个类只能实现(implements)某个接口,不能继承(extends)某个接口

4.一个类可以实现多个接口,一个类不能继承多个类但接口可以通过extends继承接口

5.接口不但可以继承接口,而且可以继承多个接口,即接口允许多继承

6.如果一个类只实现了一个接口的部分抽象方法,则该类必须的声明为抽象类。因为接口中还存在未被实现的抽象方法,java规定一个类中只要存在抽象方法,就要将该类声明为abstract。

如果不声明为抽象类,需要实现接口的所有抽象方法。因为一个抽象方法被实现后,就赋予了方法体(代码块),赋予方法体后这个方法就不是抽象方法了。当接口的所有抽象方法均被实现时,这个类中就没有抽象方法了,不需要声明成abstract了。

7.一个类可以在继承一个父类的同时实现一个或多个接口,但extends关键字必须得在implements之前

class A{}
 interface It1{}
 interface It2{}
//接口可以多重继承,即一个接口可以有多个父接口
 interface It3 extends It1, It2{}
 interface It4{
     int i = 20;
 }
//一个类可以在继承一个父类的同时实现一个或多个接口,
//但extends关键字必须的在implements之前
//此时T继承了A和所有的接口
 class T extends A implements It4,It3{}

8.不可以new接口对象(即接口不能被实例化),但可以定义一个接口引用类型的变量,并将其指向实现接口的对象,达到多态的目的!

interface It {
    void f();
}
class A implements It {
    public void f() {
        System.out.printf("AAAA");
    }
    public void g() {}
}
class D {
    public static void main(String[] args) {
        It it;
        it = new A();
        it.f();
        //it不能访问子类所特有的成员(多态注意事项)
        // it.g(); //error 
        //不可以new接口对象
        // It it2 = new It(); //error
    }
}
输出结果:AAAA

三、接口的作用

四、接口与抽象类的区别

1.接口中不允许有构造方法,抽象类允许。但两者都不能创建对象。

2.Java类不允许多继承,接口却允许多继承

  • 接口可以实现多继承,即一个接口可以有多个父类
  • 但Java类只允许单继承,即一个类只能有一个父类

五、接口小结

在Java 9+版本中,接口的内容可以有:

1.成员变量其实是常量,格式:

[public] [static] [final] 数据类型 常量名称 = 数据值;
注意:
常量必须进行赋值,而且一旦赋值不能改变。
常量名称完全大写,用下划线进行分隔。

2.接口中最重要的就是抽象方法,格式:

[public][abstract] 返回值类型 方法名称(参数列表);
注意:实现类必须覆盖重写接口所有的抽象方法,除非实现类是抽象类。

3.从Java 8开始,接口里允许定义默认方法,格式:

[public] default 返回值类型 方法名称(参数列表) { 方法体 }
注意:默认方法也可以被覆盖重写 ,且default必须写上,不能省略

4.从Java 8开始,接口里允许定义静态方法,格式:

[public]static 返回值类型 方法名称(参数列表) { 方法体 }
注意:应该通过接口名称进行调用,不能通过实现类对象调用接口静态方法

5.从Java 9开始,接口里允许定义私有很乏,格式:
普通私有方法:private 返回值类型 方法名称(参数列表) { 方法体 }
静态私有方法:private static 返回值类型 方法名称(参数列表) { 方法体 }
注意:private的方法只有接口自己才能调用,不能被实现类或别人使用。

第20课 引用类型用法总结

实际的开发中,引用类型的使用非常重要,也是非常普遍的。我们可以在理解基本类型的使用方式基础上,进一步去掌握引用类型的使用方式。基本类型可以作为成员变量、作为方法的参数、作为方法的返回值,那么当然引用类型也是可以的

一、class作为成员变量

在定义一个类Role(游戏角色)时,

定义武器类:

class Weapon {
    private String code; // 武器的代号
    public Weapon() {
    }
    public Weapon(String code) {
        this.code = code;
    }
    //get和set方法
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
}

定义角色类:

// 游戏当中的英雄角色类
class Hero {
    private String name; // 英雄的名字
    private int age; // 英雄的年龄
    //英雄的武器,此处将Weapon类作为了引用
    private Weapon weapon; 
    public Hero() {
    }
    public Hero(String name, int age, Weapon weapon) {
        this.name = name;
        this.age = age;
        this.weapon = weapon;
    }
    public void attack() {
        //weapon相当于一个对象,
        //用weapon来调用Weapon类中的getCode方法
        System.out.println("年龄为" + age + "的" + name + "用" + weapon.getCode() + "攻击敌方。");
    }
    //get和set方法
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Weapon getWeapon() {
        return weapon;
    }
//传入武器对象,赋值给第6行
    public void setWeapon(Weapon weapon) {
        this.weapon = weapon;
    }
}

定义启动类:

public class HeroTest {
    public static void main(String[] args) {
        // 创建一个英雄角色
        Hero hero = new Hero();
        // 为英雄起一个名字,并且设置年龄
        hero.setName("盖伦");
        hero.setAge(20);
        // 创建一个武器对象
        Weapon weapon = new Weapon("AK-47");
        // 为英雄配备武器
        hero.setWeapon(weapon);
        // 年龄为20的盖伦用AK-47攻击敌方。
        hero.attack();
    }
}

总结:类作为成员变量时,对它进行赋值的操作,实际上,是赋给它该类的一个对象。

二、interface作为成员变量

接口是对方法的封装,对应游戏当中,可以看作是扩展游戏角色的技能。如果想扩展更强大技能,我们在 Role 中,可以增加接口作为成员变量,来设置不同的技能。

定义技能接口:

interface Skill {
    void use(); // 释放技能的抽象方法
}

定义实现技能接口的实现类:

class SkillImpl implements Skill {
    @Override
    public void use() {
        System.out.println("Biu~biu~biu~");
    }
}

定义英雄角色:

class Hero {
    private String name; // 英雄的名称
    private Skill skill; // 英雄的技能
    public Hero() {
    }
    public Hero(String name, Skill skill) {
        this.name = name;
        this.skill = skill;
    }
    public void attack() {
        System.out.println("我叫" + name + ",开始施放技能:");
        skill.use(); // 调用接口中的抽象方法
        System.out.println("施放技能完成。");
    }
//以下是get和set方法
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Skill getSkill() {
        return skill;
    }
    public void setSkill(Skill skill) {
        this.skill = skill;
    }
}

定义启动类:

public class DemoGame {
    public static void main(String[] args) {
        Hero hero = new Hero();
        hero.setName("艾希"); // 设置英雄的名称
        // 设置英雄技能
        //使用单独定义的实现类,
        //相当于传入实现类的对象
//      hero.setSkill(new SkillImpl()); 

        // 还可以改成使用匿名内部类
//        Skill skill = new Skill() {
//            @Override
//            public void use() {
//                System.out.println("Pia~pia~pia~");
//            }
//        };
//        hero.setSkill(skill);

        // 进一步简化,同时使用匿名内部类和匿名对象
        hero.setSkill(new Skill() {
            @Override
            public void use() {
                System.out.println("Biu~Pia~Biu~Pia~");
            }
        });
        hero.attack();
    }
}
//输出结果:
//我叫艾希,开始施放技能:
//Biu~Pia~Biu~Pia~
//施放技能完成。

总结:使用一个接口,作为成员变量,以便随时更换技能,这样的设计更为灵活,增强了程序的扩展性。 接口作为成员变量时,对它进行赋值的操作,实际上,是赋给它该接口的一个子类对象。

第21课 包(package)、不同包中类的相互访问与继承

一、package的使用

package zhangsan.lisi; //1行  
 public class TestPackage{
     public static void main(String[] args){
         //匿名对象调用print()方法
         new A().print();
     }
 }
 class A{
     public void print(){
         System.out.println("AAAAA");
     }
 }

1.package语句必须得是第一条语句

2.package zhangsan.lisi表示:把该文件中所有的类(TestPackage和A)放入zhangsan.lisi这个包中,并且该文件中所有的类真正名字将是包名和类名的组合

3.如:类TestPackage 的名字将变成zhangsan.lisl.TestPackage,而不再是TestPackage。

4.编译时建议使用javac -d . TestPackagejava,尽量不要使用javac TestPackage.java。因为后者要自己手动建立包目录。

  • javac -d . A.java //中间用空格分开了的
  • -d 表示自动生成包层
  • . 表示这个包层是在当前目录下建立

5.如果不在当前路径下运行程序,则必须保证class文件的最上层目录的父目录位于classpath下。

6.java zhangsan.lisi.TestPackage解析

先检测当前目录下是否有zhangsan/lisi这个包(包即文件夹),如果有,再检测该包下是否有zhangsan.lisi.TestPackage这个类,如果没有,编译器将再去classpath设置的路径中依次查找。如果都查找失败,则运行时出错。

二、同包中不同类的相互访问

//A.java文件
class A{
     void f(){
         System.out.printf("AAAA\n");
     }
 }
//B.java文件
class B{
     public static void main(String[] args){
         A aa = new A();
         aa.f();
     }
 }
//执行命令:javac A.java B.java 
//          java B
//输出结果:AAAA

以上代码是两个java源文件,放在同一个文件夹下

1.因为类A和类B默认是在同一个无名的包中所以彼此可以相互访问,只要是非私有成员都可以被同包的另一个类访问

三、不同包中类的相互访问

//A.java文件
package zhangsan.lisi;
 public class A{
     public void f(){
         System.out.printf("AAAA");
     }
 }
//B.java文件
package com.ruide;
//方法二:使用import导入一个包中所有的类
//import zhangsan.lisi.*;
//方法三:使用import导入一个包中特定的类
import zhangsan.lisi.A;
 class B{
     public static void main(String[] args){
         //方法一:使用一个类的全名生成对象
         //zhangsan.lisi.A aa = new zhangsan.lisi.A();
         //方法二、方法三生成对象
         A aa = new A();
         aa.f();
     }
 }

1.以上代码单独编译时必须先编译A.java,再编译B.java,否则报错

建议两个文件一起编译或 javac -d . A.java B.java

javac -d . B.java A.java

2. 不同包没有任何关系的两个类,只有public类的public成员才可以被另一个包中的类访问。

四、不同包中类的继承

//A.java文件
package zhangsan.lisi
public class A{
    public void g(){
        System.out.println("GGGG");
    }
    public void b(){
        System.out.println("BBBB");
    }
}
//M.java文件
package com.ruide
import zhangsan.lisi.*
class B extends A{
    public void f(){
        g();//OK
        //子类内部可以访问从另一个包
        //继承过来的父类的public和
        //protected成员
        b();//OK
    }
}
class M{
    public static void main(String args[]){
        B bb = new B();
        bb.f();//OK 同包不同类的访问
        //子类对象可以访问从不同包的父类
        //继承过来的public成员
        bb.g();//OK 
        //子类对象不能访问从不同包的父类
        //继承过来的protected成员
        //bb.b();//error
    }
}

1.子类内部可以访问从另一个包的父类继承过来的public和protected成员

2.子类对象只能访问从另一个包的父类继承过来的public成员

五、总结:访问控制符在包的应用

1.在同一个包中只有私有的不能被另一个类访问,也只有私有的不能被继承。

2.在不同包没有任何关系的两个类,只有public类的public成员才可以被另一个包中的类访问。

3.在不同包中有继承关系的两个类,只有public类的public成员和public类的protected成员可以被另一个包中的子类在内部使用,但是在子类的外部,通过子类对象名只能访问父类的public成员。

第22课 异常

异常(Exception)是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。异常在Java中是以一个对象来看待。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行。

一、为什么需要异常

1.所有系统定义的编译和运行异常都可以由系统自动抛出,称为标准异常,但是一般情况下Java强烈建议应用程序进行完整的异常处理,给用户友好的提示,或者修正后使程序继续执行。

示例:键盘输入了不合法的数字赋值给整型变量

示例:读写错误异常的处理

二、异常的处理机制

  • 当Java程序运行时出现问题时,系统会自动检测到该错误,并立即生成一个与该错误对应的异常对象。
  • 然后将该异常对象提交给Java虚拟机。
  • Java虚拟机会自动寻找相应的处理代码来处理这个异常,如果没有找到,则由Java虚拟机做一些简单的处理后,程序被强行终止
  • 程序员可以自己编写代码来捕捉可能出现的异常,并编写代码来处理相应的异常。

三、对异常处理机制的理解

1.如果Java不提供异常处理机制,则程序一旦出错就会直接崩溃。 异常处理机制增强了程序猿对错误的处理能力,它可以保证程序出现错误时,程序猿可以捕获到这个错误,并对这个错误进行处理,处理完成后程序可以继续运行,而不是挂掉。

2.一个语句出错时,会抛出异常,整个源文件的代码被打断, JVM会将这个异常自动封装成一个对象。

3.这个异常抛给谁?

  • 先抛给这个错误代码所在的方法
  • 如果该方法没有用于捕获异常的代码,则这个方法无法处理异常,这个异常会再抛给调用该方法的方法
  • 如果仍然不捕获,则继续抛下去,抛到main方法还是未被捕获,则抛给JVM处理,JVM终止程序,并打印错误信息。

4.只有放在try中的代码抛出的异常能被捕获。

捕获异常:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。

5.放在try中的语句,编译器会做最坏的打算,编译器会假定try中的所有语句都发生异常时,整个程序能不能正常运行,如果不能,则会在编译阶段(javac)直接报错。如下面这个例子

 public class TestException {
    public static void main(String[] args) {
        //m不赋初始值会在编译阶段报错
        int m;
        try {
            m = 2;
            System.out.printf("m = %d\n", m);
        } catch (Exception e) {}
        System.out.printf("m = %d\n", m);
    }
} 
//以上代码编译阶段报错
//编译器会假设try中的代码全部发生异常
//则m可能没有初始化,拒绝打印出m的值

以下代码以try...catch语句讲解了当除数为0时产生的异常

class A{
     int divide(int a, int b){
         int m;
         //b为0,产生一个ArithmeticException异常
         m = a / b;//7行
         return m;
     }
 }
 public class TestExcep_1{
     public static void main(String[] args){
         A aa = new A();
         //try中写可能发生异常的代码
         try{
             aa.divide(6,0);//14行 传入除数0
         }
         //e用来接收被抛出的异常对象
         //catch只能捕获ArithmeticException异常
         catch (ArithmeticException e){
             System.out.println("除数不能为零, 嘿嘿嘿!");
         }   
     }
 }
 //7行发生异常
 //异常被JVM封装成对象,抛给divide方法
 //divide方法没有处理异常的代码
 //异常抛给调用divide方法的方法(本程序是main方法)
 //main方法有捕获异常的代码(本程序是catch)
 //catch方法捕获了异常并对该异常进行处理
 //运行结果:除数不能为零, 嘿嘿嘿!
 //如果main方法仍然没有捕获异常的代码
 //异常被抛给JVM处理
 //JVM终止程序,打印错误信息

四、异常的分类

0.所有的异常最终都来自Throwable类

1.Error:由Java虛拟机生成并抛出,包括动态链接失败、虚拟机错误等,Java程序无法对此错误进行处理

2.Runtime Exception:运行时异常,Java虚拟机在运行时生成的异常,如被0除等系统错误、数组下标超范围等,其产生比较频繁,处理麻烦,对程序可读性和运行效率影响太大。因此由系统检测,用户可不做处理,系统将它们交给缺省的异常处理程序(当然,必要时,用户可对其处理)。

3.Exception:一般程序中可预知的问题,其产生的异常可能会带来意想不到的结果,因此Java编译器要求Java程序必须捕获或声明所有的非运行时异常,即编译时异常

五、获取异常信息的方法

1.void printStackTrace()方法: 打印异常的详细信息并追踪错误发生的根源,将其全部打印出来,有利于程序调试。

这些信息包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace方法

2.String getMessage()方法:打印发生异常的错误信息,不会进行追踪。

class A {
    int divide(int a, int b) {
        return a / b;
    }
    public void f() {
        g();
    }
    public void g() {
        divide(6, 0);
    }
}
public class TestExcep_2 {
    public static void main(String[] args) {
        try {
            new A().f();
        } catch (Exception e) {
            //如果try中发生异常,
            //则打印异常信息
            e.getMessage();
            //此方法还会对错误进行追踪
            e.printStackTrace();
        }
    }
}

六、 编译时异常与运行时异常

1.编译时异常

(1)在Exception的子类中,除了RuntimeException类及其子类外,其他子类都是编译时异常。编译时异常的特点是在程序编写过程中,Java编译器就会对编写的代码进行检查,如果出现比较明显的异常就必须对异常进行处理,否则程序无法通过编译。

即:编译时异常程序猿必须进行处理,否则编译无法通过。

(2)处理编译时异常有两种方式

  • 使用try-catch语句对异常进行捕获处理。
  • 使用throws关键字声明抛出异常,让调用者对其处理。

2.运行时异常

(1)RuntimeException类及其子类都是运行时异常。运行时异常是在程序运行时由Java虚拟机自动进行捕获处理的,即使没有使用try..catch语句捕获或使用throws关键字声明抛出,程序也能编译通过,只是在运行过程中可能报错

(2)运行时异常一般由程序中的逻辑错误引起,在程序运行时无法恢复。常见运行时异常如下:

ArithmeticException 算术异常:除数为0
IndexOutOfBoundsException 数组下标越界
NullPointerException 空指针异常:调用了未初始化的对象或对象不存在
NumberFormatException 数字格式异常:String转换数字类型失败
IllegalArgumentException 方法参数错误

七、异常的优缺点

1.优点

  • 强制程序员考虑程序的安全性与健壮性。
  • 增强了程序员对程序的可控性,使程序不会轻易挂掉。
  • 有利于代码的调试。
  • 把错误处理代码从常规代码中分离出来。

2.缺点

  • 异常并不一定能够使程序的逻辑更清晰,因为有时我们必须得编写代码捕捉异常,所以可能会导致程序的逻辑非常混乱。
  • 异常并不能解决所有的问题。

第23课 try...catch、finally、throw、throws

一、try...catch语句

1.try...catch语句用于捕获异常,try和catch不能单独使用,必须连用,语法如下:

 try{
     //可能出现异常的代码块
 }catch(ExceptionName1 e){
     //当产生ExceptionName1异常时的处理措施
 }catch(ExceptionName2 e){
     //当产生ExceptionName2异常时的处理措施
 }
 ......
 finally{
     //无论是否捕获到异常都必须处理的代码
 }

class A {
    int divide(int a, int b) {
        int m = 0;
        m = a / b;
        return m;
    }
}

public class ExceptionFinallyTest {
    public static void main(String[] args) {
        try {
            new A().divide(6, 0);
//只有捕获到IndexOutOfBoundsException类型的异常才输出“hiahia”
//此处并不是该异常
        } catch (IndexOutOfBoundsException e) {
            System.out.printf("hiahia\n");
            e.printStackTrace();
        } catch (ArithmeticException e) {
            System.out.printf("嘿嘿嘿\n");
//finally中的语句无论捕获与否一定会执行
        } finally {
            System.out.printf("哈哈哈\n");
        }
    }
}
//输出结果:
//嘿嘿嘿
//哈哈哈

2. try...catch语句特点

(1)所有的catch只能有一个被执行,有可能所有的catch都没有执行。一旦catch执行,则执行完成后跳出try...catch语句

(2)catch与catch之间是不能有其他代码的。

(3)必须先catch子类异常再catch父类异常。如果先catch父类异常再catch子类异常,则编译时会报错。如下面这个例子,通过键盘输入a和b

import java.util.Scanner;
public class ExceptionTryCatchTest {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int a = scanner.nextInt();
        int b = scanner.nextInt();
        try {
            System.out.println(a / b);
//不能把Exception放在第一个catch当中
//因为Exception作为ArithmeticException的父类
        } catch (ArithmeticException exception) {
            System.out.println("除数不能为0");
        } catch (Exception exception) {
            System.out.println("发生异常");
        }
    }
}

二、finally代码块

1.无论try所指定的程序块中是否抛出异常,也无论catch语句的异常类型是否与所抛弃的异常的类型一致,finally中的代码一定会得到执行。除非try或者catch中调用了退出JVM的相关方法。

2.finally代码块为异常处理提供一个统一的出口,使得在控制流程转到程序的其他部分以前,能够对程序的状态作统一的管理。

3.通常在finally代码块中可以进行资源的清除工作,如关闭打开的文件、删除临时文件等。

4.使用格式:try...catch...finally,注意finally不能单独使用。

三、throw和throws关键字

用户自定义的异常和应用程序特定的异常,必须借助于throwsthrow关键字来定义抛出异常

1. throw关键字

(1)throw关键字用来抛出一个指定的异常对象,具体操作为

  • 创建一个异常对象。封装一些提示信息(信息可以自己编写)。
  • 需要将这个异常对象告知给调用者。throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

throw new NullPointerException("要访问的arr数组不存在");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");

(2)throw出去的异常,必须进行处理,一种是通过try...catch语句进行捕获处理,另一种就是通过throws关键字继续将异常声明出去。

2. throws关键字

(1)throws关键字是方法可能抛出异常的声明。用在声明方法时,表示该方法可能要抛出异常。

关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)。格式如下:

[修饰符] 返回值类型 方法名([参数列表]) throws 异常类1,异常类2,... {......}
如:public void doA(int a) throws Exception1,Exception2,Exception3{......}

(2) 在以上例子中,throws Exception1,Exception2,Exception3只是告诉程序这个方法可能会抛出这些异常,方法的调用者可能要处理这些异常,而这些异常 Exception1,Exception2,Exception3 可能是该函数体产生的。
throw则是明确了这个地方要抛出这个异常。

void doA(int a) throws Exception1,Exception3{
    try{
         ......
    }catch(Exception1 e){
         throw e;
    }catch(Exception2 e){
         System.out.println("出错了!");
    }
    if(a!=b)
         throw new Exception3("自定义异常");
}

  • 以上代码中可能会产生3个异常,(Exception1,Exception2,Exception3)。
  • 如果产生Exception1异常,则捕获之后再抛出,由该方法的调用者去处理。
  • 如果产生Exception2异常,则该方法自己处理了(即System.out.println("出错了!");)。所以该方法就不会再向外抛出Exception2异常了,void doA(int a) throws Exception1,Exception3里面的Exception2也就不用写了。
  • 而Exception3异常是该方法的某段逻辑出错,程序员自己做了处理,在该段逻辑错误的情况下抛出异常Exception3,则该方法的调用者也要处理此异常。

3.throw与throws的区别

(1)throw语句用在方法体内,表示抛出异常,由方法体内的语句处理。
throws语句用在方法声明后面,表示再抛出异常,由该方法的调用者来处理(如main方法调用成员方法)。

(2)throws主要是声明这个方法可能会抛出这种类型的异常,使它的调用者知道要捕获这个异常。
throw是具体向外抛异常的动作,所以它是抛出一个异常实例。

(3) throws表示出现异常的一种可能性,并不一定会发生这些异常

throw则是抛出了异常,执行throw则一定抛出了某种异常。

(4)两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

四、 throws的异常声明详解

1. 首先,throws抛出的是一个异常类对象,这个对象带有所发生异常的信息。我们可以直接使用Java中已经封装好的异常信息类,也可以自己去定义一个异常类。

2.案例1:Java中已经定义好的异常类

IOException:输入输出流异常

import java.io.IOException;
public class Test {
    public void Abnormal() throws IOException {
        int i = 0;
        int x = 5 / i;
        System.out.println(x);
    }
    public static void main(String[] args) throws IOException {
        Test t = new Test();
        t.Abnormal();
    }
}
//输出结果:
//Exception in thread "main" java.lang.ArithmeticException: / by zero
//	at Test.Abnormal(Test.java:8)
//	at Test.main(Test.java:13)

分析:这里直接使用了JAVA中的IOException异常类对象,由于在main函数中没有对这个异常进行处理,所以我们要给main函数加上throws IOException,指明我不想处理这个异常,请帮我把它抛给上一级。于是这个异常就被抛给了JAVA虚拟机,JAVA虚拟机根据IOException所带的异常信息,判断这是一个整数除以0的异常,于是终止程序,并且打印出"/ by zero"的报错信息。

3.案例2: 自己定义一个异常类 (一般不这么用)

import java.io.IOException;
public class Test {
    public void Abnormal() throws IOException {
        int i = 0;
        if (0 == i) {
            throw new IOException("除数不能为0");
        }
        int x = 5 / i;
        System.out.println(x);
    }
    public static void main(String[] args) throws IOException {
        Test t = new Test();
        t.Abnormal();
    }
}
//输出结果:
//Exception in thread "main" java.io.IOException: 除数不能为0
//	at daily.Test.Abnormal(Test.java:9)
//	at daily.Test.main(Test.java:16)

如果我们需要抛出一个具体的异常信息,可以用JAVA中已有的异常类构造一个新的异常类对象,把相应的异常信息传给它就可以了。 这样子我们就可以打印出我们所想要的具体异常信息了,更加人性化。以上代码最终还是交给了JVM处理,只是输出的异常信息不同。

五、通过try...catch、throw、throws的组合来处理异常

1.假设f方法抛出了一个A异常,则f方法有两种方式来处理A异常

(1)void f() throws A

  • 谁调用f方法,谁处理A异常,f方法本身不处理A异常

(2)在f方法中使用try...catch语句来自己处理异常,此时不需要在声明方法时加上throws A

2.以下代码,在main函数中加入了try...catch语句来处理异常,则不在main函数后面声明throws, 且注意到输出结果没有打印出"除以0错误",说明这个异常在main函数中处理完就跳出了try...catch语句,没有继续往上抛给JVM,从而没有打印出 "除以0错误" 。

import java.io.IOException;
public class Test {
    public void Abnormal() throws IOException {
        int i = 0;
        if (i == 0) {
            throw new IOException("除以0错误");
        }
        int x = 5 / i;
        System.out.println(x);
    }
    public static void main(String[] args) {
        try {
            Test t = new Test();
            t.Abnormal();
        } catch (IOException e) {
            System.out.println("出现了IOException异常");
        } catch (NullPointerException e) {
            System.out.println("出现了空指针异常");
        }
    }
}
//输出结果:出现了IOException异常

3. 总结: throw/throws 与 try...catch 的关系

throw/throws就是一个甩锅的,它只会把异常一层层地往上抛,直到有人去处理它。而try...catch就是那个背锅的,负责捕获相应的异常并对它进行处理。

六、异常注意事项

1.一个方法throws Exception1,该方法可以不抛出Exception1异常,调用该方法的方法也可以不处理Exception1异常

2.运行时异常被抛出可以不处理,即不捕获也不声明抛出。

3. 如果finally代码块中有return语句catch代码块中也有return语句,无论是否捕获到异常,永远返回finally中的结果

4. 如果父类抛出了多个异常,类重写父类方法时,抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。

5. 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出。

第24课 自定义异常

一、自定义异常

1.JDK中定义了大量的异常类,虽然这些异常类可以描述编程时出现的大部分异常情况,但是在程序开发中有时可能需要描述程序中特有的异常情况,例如在设计divide()方法时不允许被除数为负数(以下案例2)。为了解决这个问题,在Java中允许用户自定义异常,自定义的异常类必须继承自Exception类或其子类

2.自定义异常类在没有特殊需求的情况下一般都继承Exception类,其实质是通过super关键字调用Exception类的构造函数Exception(String message)以达到自定义异常的具体信息的目的。

3.创建并使用自定义异常遵循以下四个步骤:

  • 首先创建自定义异常类,语法格式:自定义异常类名 extends Exception。
  • 在方法中通过关键字throw抛出自定义异常对象。
  • 若是在当前抛出异常的方法中处理异常,可以用try...catch语句捕获并处理;若不是,在方法的声明处通过throws关键字指明可能抛出这个异常。
  • 在出现异常方法的调用中捕获并处理异常。

案例1:Exception类中有一个private的Message属性,通过构造函数 public Exception(String message),可以完成对该属性的初始化

class E extends Exception {
     public E() {
//调用父类Exception的构造函数: public Exception(String message)
         super("哈哈"); 
         //this.message = "哈哈!"; //error
     }
 }
 class Excep_3{
     public static void f() throws E // 也可以改为 throws Exception
     {
         throw new E();//把E抛给main方法,注意new不能丢
     }
     public static void main(String[] args) {
         try {
             f();
         } catch (Exception e) {
             String strExcep = e.getMessage();
             System.out.println("strExcep = " + strExcep);
         }
//对异常处理后不影响try...catch后面的语句的执行
     System.out.println("程序正常终止了!"); }
 }
//输出结果:
//strExcep = 哈哈! 
//程序正常终止了!

以上代码说明了getMessage() 返回的是异常的具体信息,是个String类型。

  • public Exception(String message) 用message字符串来表示异常的具体信息

案例2: divide()方法不允许被除数为负数。

在main()方法中,定义了一个try…catch语句用于捕获divide()方法抛出的异常。在调用divide()方法时由于传入的被除数不能为负数,程序会抛出一个自定义异常DivideIsMinusException,该异常被捕获后最终被catch代码块处理,并打印出自定义的异常信息

class DivisorIsMinusException extends Exception {
    public DivisorIsMinusException(String message) {
//通过super调用父类Exception的构造方法public Exception(String message)
        super(message);
    }
}

public class Test {
    public static void divide(int a, int b) throws DivisorIsMinusException {
        if (b < 0)
//将Exception对象的错误信息重写为"除数不能为负数!"
            throw new DivisorIsMinusException("除数不能为负数!");
    }

    public static void main(String[] args) {
        try {
            divide(3, -2);
        } catch (DivisorIsMinusException e) {
//打印异常对象的错误信息"除数不能为负数!"
            System.out.println(e.getMessage());
        }
        //对异常处理后不影响try...catch后面的语句的执行
        System.out.println("程序正常终止了!");
    }
}
//输出结果:
//除数不能为负数!
//程序正常终止了!

第25课 内部类

内部类:将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类。

内部类的作用:

1.内部类可以很好的实现隐藏
一般的非内部类,是不允许有 private 与protected权限的,但内部类可以
2.内部类拥有外部类的所有元素的访问权限 (private修饰也能访问)
3.可以实现多重继承 (让多个内部类分别继承多个其他类,使外部类可以同时获取多个其他类的属性)
4.可以避免修改接口而实现同一个类中两种同名方法的调用。(外部类继承,让内部类实现接口)

一、成员内部类

1.成员内部类 :定义在类中方法外的类。

2.格式:

class Car { //外部类:小汽车
    class Engine { //内部类:发动机
    } 
}

3.访问特点

  • 内部类可以直接访问外部类的成员,包括私有成员。
  • 外部类要访问内部类的成员,必须要建立内部类的对象。

(1)使用成员内部类

间接方式:在外部类的方法中,调用内部类。main方法中只调用外部类的方法。

直接方式:创建内部类对象,格式如下

外部类名.内部类名 对象名 = new 外部类型().new 内部类型();

public class Body { // 外部类
    public class Heart { // 成员内部类
        // 内部类的方法
        public void beat() {
            System.out.println("心脏蹦蹦跳");
            System.out.println("我叫:" + name); // 正确写法!
        }
    }
    // 外部类的成员变量
    private String name;
    // 外部类的方法
    public void methodBody() {
        System.out.println("外部类的方法");
        new Heart().beat();
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
public class InnerClassTest {
    public static void main(String[] args) {
        Body body = new Body(); // 外部类的对象
        // 通过外部类的对象,调用外部类的方法,里面间接在使用内部类Heart
        body.methodBody();
        System.out.println("========");

        // 按照公式写:
        Body.Heart heart = new Body().new Heart();
        //也可以这么写:
        //Body.Heart heart = body.new Heart();
        heart.beat();
    }
}

4.内部类的同名变量访问

如果内部类和外部类有重名变量,调用内部类变量的格式是:

外部类名称.this.外部类成员变量名

public class Outer {
    int num = 10; // 外部类的成员变量
    public class Inner  {
        int num = 20; // 内部类的成员变量
        public void methodInner() {
            int num = 30; // 内部类方法的局部变量
            System.out.println(num); // 局部变量,就近原则,输出30
            System.out.println(this.num); // 内部类的成员变量,输出20
            System.out.println(Outer.this.num); // 外部类的成员变量,输出10
        }
    }
}

二、局部内部类

1.在方法内部定义的类,叫局部内部类。

只有该方法内部可以访问

2.定义格式:

class Outer {
    public void methodOuter() {
        class Inner { // 局部内部类
            int num = 10;
            public void methodInner() {
                System.out.println(num); // 10
            }
        }
        //只能在方法内部访问Inner
        Inner inner = new Inner();
        inner.methodInner();
    }
}

3.注事事项

局部内部类,如果希望访问所在方法的局部变量,那么这个局部变量必须是final的。

PS:从Java 8+开始,只要局部变量事实不变,那么final关键字可以省略。

原因:

这是作用域的问题。在方法methodOuter()执行完成后,局部变量num就失效了,而在new Inner()产生的inner对象还存在于堆中,这样对象就访问了一个不存在的变量,是不允许的。inner还存在,在外面和后续调用该局部变量时,这个局部变量可能已经失效了。但为什么加上final就可以保证能访问呢?这里Java采用了一种copy local variable的方法实现,定义为final的变量,会拷贝一份存到局部内部类中,后续使用持续维护这个对象在生命周期内,所以可以继续访问。

三、匿名内部类(重点)

1.匿名内部类: 匿名内部类就是没有名字的内部类,是内部类的简化写法。它的本质是一个实现了父类或者父接口的匿名子类对象
开发中,最常用到的内部类就是匿名内部类了。

以接口为例,当你使用一个接口时,似乎得做如下几步操作:

  • 定义实现类
  • 在实现类中重写接口中的方法
  • 创建实现类对象
  • 调用重写后的方法

我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。

2.前提:匿名内部类必须继承一个父类或者实现一个父接口

啥时候用?如果接口的实现类(或者是父类的子类)只需要使用唯一的一次,那么这种情况下可以省略该类的定义,而使用匿名内部类。

3.格式

new 父类名或者接口名(){ 
    // 方法重写 
    @Override 
    public void method() { 
        // 执行语句
    } 
};

4.使用方式

(1)创建匿名内部类并调用

//定义接口
interface FlyAble {
    public abstract void fly();
}
public class Temp {
    public static void main(String[] args) {
/* 1.等号右边:是匿名内部类,定义并创建该接口的子类对象
2.等号左边:是多态赋值,接口类型引用指向子类对象 */
        FlyAble f = new FlyAble() {
            @Override
            public void fly() {
                System.out.println("我飞了~~~");
            }
        };
//调用 fly方法,执行重写后的方法
        f.fly();
    }
}

(2)通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。

interface FlyAble {
    public abstract void fly();
}

public class Temp {
    public static void main(String[] args) {
        FlyAble f = new FlyAble() {
            @Override
            public void fly() {
                System.out.println("我飞了~~~");
            }
        };
//将f指向的匿名内部类作为参数,
//传递给showFly方法中
        showfly(f);
    }
    public static void showFly(FlyAble f) {
        f.fly();
    }
}

以上两步可以简化为一步

interface FlyAble {
    public abstract void fly();
}
public class Temp {
    public static void main(String[] args) {
//创建匿名内部类作为参数,直接传递给showFly(FlyAble f) 
        showFly(new FlyAble() {
            @Override
            public void fly() {
                System.out.println("我飞了~~~");
            }
        });
    }
    public static void showFly(FlyAble f) {
        f.fly();
    }
}

5.对匿名内部类的理解

对格式“new 接口名称() {…}”进行解析:

  1. new代表创建对象的动作
  2. 接口名称就是匿名内部类需要实现哪个接口
  3. {…}这才是匿名内部类的内容

6.匿名内部类的注意事项

  1. 匿名内部类,在【创建对象】的时候,只能使用唯一一次
    如果希望多次创建对象,而且类的内容一样的话,那么就需要使用单独定义的实现类了。
  2. 匿名对象,在【调用方法】的时候,只能调用唯一一次
    如果希望同一个对象,调用多次方法,那么必须给对象起个名字。
  3. 匿名内部类是省略了【实现类/子类名称】,但是匿名对象是省略了【对象名称】
    强调:匿名内部类和匿名对象不是一回事!!!

interface MyInterface {
    void method1(); // 抽象方法
    void method2();
}
//一般情况的实现接口
class MyInterfaceImpl implements MyInterface {
    @Override
    public void method1() {
        System.out.println("实现类覆盖重写了方法!111");
    }
    @Override
    public void method2() {
        System.out.println("实现类覆盖重写了方法!222");
    }
}

public class Temp {
    public static void main(String[] args) {
//        MyInterface obj = new MyInterfaceImpl();
//        obj.method1();
//        MyInterface some = new MyInterface(); // 错误写法!

// 使用匿名内部类,但不是匿名对象,对象名称就叫objA
        MyInterface objA = new MyInterface() {
            @Override
            public void method1() {
                System.out.println("匿名内部类实现了方法!111-A-method1");
            }

            @Override
            public void method2() {
                System.out.println("匿名内部类实现了方法!222-A-method2");
            }
        };
        objA.method1();
        objA.method2();
        System.out.println("=================");

//使用了匿名内部类,而且省略了对象名称,也是匿名对象
        new MyInterface() {
            @Override
            public void method1() {
                System.out.println("匿名内部类实现了方法!111-B-method1");
            }

            @Override
            public void method2() {
                System.out.println("匿名内部类实现了方法!222-B-method2");
            }
        }.method1();
        System.out.println("=================");
//因为匿名对象无法调用第二次方法,所以需要再创建一个匿名内部类的匿名对象
        new MyInterface() {
            @Override
            public void method1() {
                System.out.println("匿名内部类实现了方法!111-C-method1");
            }
            @Override
            public void method2() {
                System.out.println("匿名内部类实现了方法!222-C-method2");
            }
        }.method2();
    }
}
//输出结果:
//匿名内部类实现了方法!111-A-method1
//匿名内部类实现了方法!222-A-method2
//=================
//匿名内部类实现了方法!111-B-method1
//=================
//匿名内部类实现了方法!222-C-method2

4.匿名内部类不能定义任何静态成员、方法。

5.匿名内部类中的方法不能是抽象的。

6.匿名内部类必须实现接口或抽象父类的所有抽象方法。

7.匿名内部类访问的外部类成员变量或成员方法必须用static修饰。

第26课 Lambda表达式入门、 函数式接口

Lambda表达式是Java8的重量级新特性,打开了新世界的大门,它运用了函数式编程思想,基于函数式接口实现,而尽量忽略面向对象的复杂语法。Lambda表达式的应用非常广泛,本课内容作为初次邂逅Lambda表达式,仅介绍其基本格式与实现匿名内部类,Lambda表达式的详细讲解将放在后续的笔记当中。

一、Lambda表达式实现匿名内部类

1.当一个接口只有一个抽象方法时,可以使用Lambda表达式。

2.Lambda表达式的格式:( 参数列表 ) -> {表达式主体}

参数列表:对应的就是需要重写接口方法的形参,多个参数用逗号隔开。

省略格式:参数的数据类型可以省略如果只有一个参数,小括号可以省略

表达式主体:对应的就是接口或抽象类的实现方法体

省略格式: 如果表达式只有一条语句,大括号和语句结尾的分号可以省略。如果只有一条语句并且是return语句时,return关键字也可以省略。 注意事项是,要省略就一起省略,不能只省略某一个。

3.本质:接口中抽象方法的具体实现

4.Lambda返回的是一个匿名对象,其效果与匿名内部类的效果相同,即使你没有写return语句。

下面演示用Lambda表达式实现匿名内部类

interface FlyAble {
    public abstract void fly();
}
public class Temp {
    public static void main(String[] args) {
// 创建匿名内部类作为参数,直接传递给showFly(FlyAble f)
        showFly(new FlyAble() {
            @Override
            public void fly() {
                System.out.println("我飞了~~~");
            }
        });
//使用Lambda表达式作为参数,直接传递给showFly(FlyAble f) 
        showFly(()->{System.out.println("我飞了~~~");});
    }
    public static void showFly(FlyAble f) {
        f.fly();
    }
}

以上两种方式重写接口中的抽象方法,效果是一样的。很显然使用Lambda表达式更加简洁。

二、 Lambda表达式与匿名内部类

联系:

  1. Lambda就是对匿名内部类的一种简化操作, 可以部分取代匿名内部类。
  2. Lambda与匿名内部类一致可以访问外部类的成员变量。
  3. Lambda创建的对象和匿名内部类创建的对象一致, 可直接调用接口中继承的方法。

区别:

  1. 匿名内部类可以为任意接口创建实例, 不管接口中具有多少个抽象方法, 但是Lambda只能为单个抽象方法创建实例
  2. 匿名内部类可以为抽象类或者普通类创建实例, Lambda只能为函数式接口创建实例
  3. 匿名内部类允许调用接口中的默认方法, 但是Lambda不行,只能调用单个抽象方法。

三、函数式接口

虽然Lambda表达式可以实现匿名内部类的功能,但在使用时却有一个局限,即接口中有且只有一个抽象方法时才能使用Lamdba表达式代替匿名内部类。这是因为Lamdba表达式是基于函数式接口实现的,所谓函数式接口是指有且仅有一个抽象方法的接口,Lambda表达式就是函数式编程的提现,为了确保接口中有且只有一个抽象方法,引入了@FunctionalInterface注解确保一个接口是函数式接口,它会检查一个接口是否只有一个抽象方法,如果不是1个就会报错。

第27课 方法引用与构造器引用

说明:Lambda表达式的主体只有一条语句时,程序不仅可以省略包含主体的花括号,还可以通过英文双冒号“::”的语法格式来引用方法和构造器(即构造方法)

作用:可以进一步简化Lambda表达式的书写,其本质都是对Lambda表达式的主体部分己存在的方法进行直接引用,主要区别就是对普通方法与构造方法的引用而已。

一、类名引用静态方法

类名引用静态方法也就是通过类名对静态方法的引用,该类可以是Java自带的特殊类,也可以是自定义的普通类。

//函数式接口
@FunctionalInterface
interface Calcable {
    int calc(int num);
}
// 定义一个类,并在类中定义一个静态方法
class Math {
    // 定义一个求绝对值方法
    public static int abs(int num) {
        if (num < 0) {
            return -num;
        } else {
            return num;
        }
    }
}
// 定义测试类
public class ClassNameTest {
//一个打印绝对值的方法
    private static void printAbs(int num, Calcable calcable) {
        System.out.println(calcable.calc(num));
    }
    public static void main(String[] args) {
        // 使用Lambda表达式方式
        printAbs(-10, n -> Math.abs(n));
        // 使用方法引用的方式
        printAbs(-10, Math::abs);
    }
}

二、 对象名引用普通方法

对象名引用方法指的是通过实例化对象的名称来对其方法进行的引用。

//定义一个函数式接口
@FunctionalInterface
interface Printable {
    void print(String str);
}

// 定义一个类,里面有转换成大写的方法
class StringUtils {
    public void printUpperCase(String str) {
        System.out.println(str.toUpperCase());
    }
}

// 定义测试类
public class Example25 {
    // 打印大写字母方法
    private static void printUpper(String text, Printable pt) {
        pt.print(text);
    }
    public static void main(String[] args) {
        StringUtils stu = new StringUtils();
        // 使用Lambda表达式方式
        printUpper("Hello", (String t1) -> stu.printUpperCase(t1));
        // 使用方法引用的方式
        printUpper("Hello", stu::printUpperCase);
    }
}

三、构造器引用方法

对类自带的构造函数的引用

//定义一个函数式接口
@FunctionalInterface
interface PersonBuilder {
    Person buildPerson(String name);
}
// 定义一个Person类,并添加有参构造方法
class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
// 定义测试类
public class ConstructorTest {
    public static void printName(String name, PersonBuilder builder) {
        System.out.println(builder.buildPerson(name).getName());
    }
    public static void main(String[] args) {
        // 使用Lambda表达式方式
        printName("赵丽颖", name -> new Person(name));
        // 使用构造器引用的方式
        printName("赵丽颖", Person::new);
    }
}

四、类名引用普通方法

建议不用,因为太复杂,用对象名引用普通方法最好

//定义一个函数式接口
@FunctionalInterface
interface Printable {
    void print(StringUtils su, String str);
}
class StringUtils {
    public void printUpperCase(String str) {
        System.out.println(str.toUpperCase());
    }
}

// 定义测试类
public class Test {
    private static void printUpper(StringUtils su, String text, Printable pt) {
        pt.print(su, text);
    }
    public static void main(String[] args) {
        // 使用Lambda表达式方式
        printUpper(new StringUtils(), "Hello", (object, t) -> object.printUpperCase(t));
        // 使用方法引用的方式
        printUpper(new StringUtils(), "Hello", StringUtils::printUpperCase);
    }
}

第28课 可变参数

1.如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,但是参数的个数不确定,就可以使用可变参数。格式如下:

修饰符 返回值类型 方法名(参数类型... 形参名){  }

2.可变参数的原理

可变参数底层就是一个数组,根据传递参数个数不同,会创建不同长度的数组,来存储这些参数。传递的参数个数,可以是0个(不传递),1个,2个…多个。

例如:计算未知个整数的和

public class VariableArgsTest {
    public static void main(String[] args) {
        int sum = add(1,2,3,4,5);
        System.out.println(sum);//15
    }

    //计算未知个整数的和
    public static int add(int... args) {
        System.out.println(args);//[I@10f87f48,可见args底层是个数组
        System.out.println(args.length);//5
        int sum = 0;
        for (int i : args) {
            sum += i;
        }
        return sum;
    }
}

3.可变参数的注意事项

  • 一个方法的参数列表,只能有一个可变参数
  • 如果方法的参数有多个,那么可变参数必须写在参数列表的末尾
  • 带有可变参数的方法,不能重载,因为会发生调用的不确定性。

JavaSE面向对象部分的笔记至此完结,感谢您的阅读。继续阅读Java常用类、集合、I/O的笔记请移步JavaSE笔记:开发基础篇

Copyright © 2020-2021 AnonyEast, All rights reserved.

 

AnonyEast

一个爱折腾的技术萌新

2 Comments

  • 我又来惹(๑´∀`๑)

    • @宇宙无敌超级可爱温柔善良贤惠大方独一无二你缺一不可的小仙女是也 以后常来哈哈哈

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐