Bootstrap

Java基础笔记(初学者适用)

目录

一、Java主函数解析

二、Java学习注意事项

三、JDK和JRE的区别

1、jdk--开发环境(核心)

2、jre--运行环境

3、JVM——转换环境

四、常见转义字符

五、变量

1.变量介绍

2.变量细节

3.作用域

4.数据类型

(1)Java数据类型图解

(2)整数类型

(3)浮点类型

(4)字符类型

(5)布尔类型

(6)基本数据类型转换

六、Java中运算符的使用

1.“+”号的使用

2.算术运算符

3.逻辑运算符

4.位运算符

七、键盘输入语句

八、控制结构

九、数组

1.动态初始化

2.静态初始化

3.数组赋值

4.多维数组-二维数组

十、排序

冒泡排序

十一、面向对象之类与对象

1、面向对象简述

2、类与对象的基本概念

3、类与对象的定义和使用

4、对象引用传递初步分析

5、方法的重载

6、可变参数

7、构造器

8、this关键字

9、访问修饰符

10、封装

11、继承

12、supper关键字

13、多态

十二、引用类型转换

十三、抽象类

十四、接口

十五、抽象类和接口的区别

十六、递归

一、例题


一、Java主函数解析

主函数的一般写法如下:

public static void main(String[] args){…}
public static void main(String args[]){…}

下面分别解释这些关键字的作用:

(1)public关键字,这个好理解,声明主函数为public就是告诉其他的类可以访问这个函数。

(2)static关键字,告知编译器main函数是一个静态函数。也就是说main函数中的代码是存储在静态存储区的,即当定义了类以后这段代码就已经存在了。如果main()方法没有使用static修饰符,那么编译不会出错,但是如果你试图执行该程序将会报错,提示main()方法不存在。因为包含main()的类并没有实例化(即没有这个类的对象),所以其main()方法也不会存。而使用static修饰符则表示该方法是静态的,不需要实例化即可使用。

(3)void关键字表明main()的返回值是无类型。

(4)  参数args的主要作用是为程序使用者在命令行状态下与程序交互提供了一种手段。此外在其他类中直接使用main()函数,并传递参数也是可行的,虽然这种方法不太常用,但毕竟为我们提供了一种选择。

二、Java学习注意事项

1、Java文件名要与主类名一致

三、JDK和JRE的区别

JDK:java development kit (java开发工具)

JRE:java runtime environment (java运行时环境)

JVM:java virtuak machine (java虚拟机)

1、jdk--开发环境(核心)

Java development kit的缩写,意思是Java开发工具,我们写文档做PPT需要office 办公软件,开发当然需要开发工具了,说到开发工具大家肯定会想到Eclipse,但是如果直接安装Eclipse你会发现它是运行不起来 是会报错的,只有安装了JDK,配置好了环境变量和path才可以运行成功。这点相信很多人都深有体会。

jdk主要包含三个部分:

第一部分是Java运行时环境,JVM

第二部分是Java的基础类库,这个类库的数量还是相当可观的

第三部分是Java的开发工具,它们都是辅助你更好地使用Java的利器

2、jre--运行环境

① jdk中的jre

如下图:jdk中包含的jre,在jre的bin目录里有个jvm.dll,既然JRE是运行时环境,那么运行在哪?肯定是JVM虚拟机上了。另,jre的lib目录中放的是一些JAVA类库的class文件,已经打包成jar文件。

② 第二个JRE(独立出来的运行时环境)

如下图,不管是JDK中的JRE还是JRE既然是运行时环境必须有JVM。所以JVM也是有两个的。、

3、JVM——转换环境

java virtuak machine (java虚拟机)的缩写。

大家一提到JAVA的优点就会想到:一次编译,随处运行,说白了就是跨平台性好,这点JVM功不可没。

Java的程序也就是我们编译的代码都会编译为class文件,class文件就是在jvm上运行的文件,只有JVM还不能完全支持class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib,而jre包含lib类库。

JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改的运行。

JVM也是一门很深的学问,感兴趣的同学可以深入研究,只有好处,没有坏处。

其实有时候面试官问JDK和JRE的区别的目的不是想让你解释什么名词的,而是想看看你的基础和研究Java的深浅,还有另一方面就是你是不是经常喜欢问为什么。

四、常见转义字符

转义字符(ASCII码值[十进制])

意义

\b(008)

退格(BS),将当前位置移到前一列

\f(012)

换页(FF),将当前位置一道下页开头

\n(010)

换行(LF),将当前位置移到下一行开头

\r(013)

回车(CR),将当前位置移到本行开头

\t(009)

水平制表(HT)(跳到次啊一个TAB位置)

\\(092)

代表一个反斜字符\

\’(039)

代表一个单引号(撤号字符)

\ddd(三位八进制)

1到3位八进制数所代表的任意字符

\xhh(十六进制)

十六进制所代表的任意字符

五、变量

1.变量介绍

(1)变量的概念

变量相当于内存中一个数据存储空间的表示,你可以把变量看做是一个房间的门牌号,通过门牌号我们可以找到房间,而通过变量名可以访问到变量(值)。

2.变量细节

(1)变量必须先声明,后使用,不然会报错。

(2)变量表示内存中的一个存储区域[不同的变量,类型不同,占用的空间大小不同,比如: int 4个字节,double就是8个字节,先有基本印象,后面说字节]

(3)该区域有自己的名称[变量名]和类型[数据类型]

(4)变量必须先声明,后使用,即有顺序

(5)该区域的数据/值可以在同一类型范围内不断变化5.变量在同一个作用域内不能重名

(6)变量=变量名+值+数据类型,这一点请大家注意。变量三要素

3.作用域

3.1 基本使用

(1)在Java编程中,主要的变量就是属性(成员变量)和局部变量。

(2)我们说的局部变量一般是指在成员方法中定义的变量。

(3)java中的作用域的分类

全局变量:也就是属性,作用域为整个类体。

局部变量:也就是除了属性之外的其他变量,作用域为定义它的代码块中!

(4)全局变量可以不赋值,直接使用,因为有默认值( 0.0 ),局部变量必须赋值后,才能使用,因为没有默认值。

3.2 注意事项和细节使用

(1)属性和局部变量可以重名,访问时遵循就近原则。

(2)在同一个作用域中,比如在同一个成员方法中,两个局部变量,不能重名。

(3)属性生命周期较长,伴随着对象的创建而创建,伴随着对象的死亡而死亡。局部变量,生命周期较短,伴随着它的代码块的执行而创建,伴随着代码块的结束而死亡,即在一次方法调用过程中。

(4)作用域范围不同

全局变量/属性:可以被本类使用,或其他类使用(通过对象调用)。

局部变量:只能在本类中对应的方法中使用。

(5)修饰符不同

全局变量/属性可以加修饰符。

局部变量不可以加修饰符。

4.数据类型

每一种数据都定义了明确的类型,在内存中分配了不同大小的内存空间(字节)。

(1)Java数据类型图解

(2)整数类型

整型的使用细节IntDetail.java

1. Java各整数类型有固定的范围和字段长度,不受具体OS[操作系统]的影响,以

保证java程序的可移植性。

2. Java的整型常量默认为int型,声明long型常量须后加“l”或“L”。

3. java程序中变量常声明为int型,除非不足以表示大数,才使用long。

4. bit:计算机中的最小存储单位。byte:计算机中基本存储单元, 1byte = 8 bit。

[二进制再详细说,简单举例一个byte 3和short 3 ]

(3)浮点类型

基本介绍

Java的浮点类型可以表示一个小数,比如123.4,7.8,0t12等等

浮点型的分类

类型

占用存储空间

范围

单精度float

4字节

-3.403E38~ 3.403E38

双精度double

8字节

-1.798E308~ 1.798E308

说明一下:

1.关于浮点数在机器中存放形式的简单说明,浮点数=符号位+指数位+尾数位

2.尾数部分可能丢失,造成精度损失(小数都是近似值)。

浮点类型使用细节

浮点型使用细节 FloatDetail.java
1.与整数类型类似,Java浮点类型也有固定的范围和字段长度,不受具体OS的
影响。[float 4个字节double是8个字节]】


2. Java的浮点型常量(具体值)默认为double型,声明float型常量,须后加‘f'或‘F'


3.浮点型常量有两种表示形式
十进制数形式:如:5.12512.0f.512(必须有小数点)
科学计数法形式:如:5.12e2 [5.12*10的2次方]5.12E-2[5.12/10的2次方

public class FloatDetail {
    //编写一个main方法
    public static void main(String[] args){
        //Java的浮点型常量(具体值)默认为double型,声明float型常量,须后加'f'或'F'
        //float num1 = 1.1; //错误
        float num2 = 1.1F; //正确
        double num3 = 1.1; //正确
        double num4 = 1.1f; //正确
        
        //十进制数形式:如:5.12     512.0f          .512  (必须有小数点)
        double num5 = .123;  //等价 0.123
        System.out.println(num5);
        //科学计数法形式:如:5.12e2 [ 5.12 * 10的2次方 ]    5.12E-2 [ 5.12 * 10的-2次方]
        System.out.println(5.12e2);     //512.0
        System.out.println(5.12E-2);     //0.0512
    }
}

4、通常情况下,应该使用double型,因为它比float型更精确。

[举例说明]

double num9 = 2.1234567851;

输出结果: 2.1234567851


float num10 = 2.1234567851F;

输出结果: 2.1234567


5.浮点数使用陷阱:2.7和8.1/3比较

 
//浮点数使用陷阱:2.7和8.1/3比较
        double num11 = 2.7;
        double num12 = 8.1 / 3;
        System.out.println(num11);  //2.7
        System.out.println(num12);  //2.6999999999999997
        //得到一个重要的使用注意点:当我们对运算结果是小数的进行相等判断时候,要小心
        //应该是以两个数的差值的绝对值,在某个精度范围判段

        //错误的写法
        if( num11 == num12) {
            System.out.println("相等");
        }
        //正确的写法
        if(Math.abs(num11 - num12) < 0.000001) {
            System.out.println("相等");
            System.out.println(Math.abs(num11 - num12));
        }

(4)字符类型

基本介绍

字符类型可以表示单个字符,字符类型是char, char是两个字节(可以存放汉字),多个字符我们用字符串String(我们后面详细讲解String)

字符类型使用细节
1.字符常量是用单引号(' ')括起来的单个字符。例如:
char c1 = 'a';

char c2 ='中';

char c3 = '9";


2.Java中还允许使用转义字符 ' \ ' 来将其后的字符转变为特殊字符型常量。

例如:char c3 = ' \n '; // ' \n '表示换行符。

3.在java中,char的本质是一个整数,在输出时,是unicode码对应的字符。
Unicode编码转换 - 站长工具


4.可以直接给char赋一个整数,然后输出时,会按照对应的unicode字符输出。

[97-》a]

5. char类型是可以进行运算的,相当于一个整数,因为它都对应有Unicode码。

样例如下所示:

public class CharDetail {

    public static void main(String[] args) {
        char c1 = 97;
        System.out.println(c1);         //a
        System.out.println((int)c1);    //97

        // char类型可以进行运算,相当于一个整数,因为它都对应有Unicode码。
        System.out.println('a' + 10);   //107

        char c5 = 'b' + 1;
        System.out.println((int)c5);    //99
        System.out.println(c5);         //c
    }
}

字符类型本质探讨

1.字符型存储到计算机中,需要将字符对应的码值(整数)找出来,比如'a'

存储:'a'==>码值97==>二进制(110 0001) ==>存储

读取:二进制(110 0001)=>97 ===> 'a'=>显示

2.字符和码值的对应关系是通过字符编码表决定的(是规定好)

介绍一下字符编码表[sublime测试]

ASCl (ASCIl编码表一个字节表示,一个128个字符,实际上一个字节可以表示256个字符,只用128个)lInicede (Unicode编码表固定大小的编码使用两个字节来表示字符,字母和汉字统一都是占用两个字节,这样浪费空间)

utf-8(编码表,大小可变的编码字母使用1个字节,汉字使用3个字节)

gbk(可以表示汉字,而且范围广,字母使用1个字节,,汉字2个字节)

gb2312(可以表示汉字,gb2312< gbk)

big5码(繁体中文,台湾,香港)

(5)布尔类型

基本介绍

1.布尔类型也叫boolean类型,booolean类型数据只允许取值true和false,无null

2.boolean类型占1个字节。

3. boolean类型适于逻辑运算,一般用于程序流程控制[这个后面会详细介绍]:√if件控制语句;

√ while循环控制语句;

√ do-while循环控制语句;

√ for循环控制语句

public class Boolean01 {
    public static void main(String[] args) {
        boolean ispass = true;
        if(ispass) {
            System.out.println("恭喜你考试通过!");
        } else {
            System.out.println("很遗憾,这次没有通过~");
        }
    }
}

使用细节说明

不可以用0或者1以及非零的数来代替false和true,这点和C语言截然相反。

(6)基本数据类型转换

自动类型转换

√ 介绍:

当java程序在进行赋值或者运算时,精度小的类型自动转换为精度大的数据类型,这个就是自动类型转换。

√ 数据类型按精度(容量)大小排序为(背, 规则)

public class AutoConvert {
    public static void main(String[] args) {
        //自动转换
        int num = 'a';  //ok char -> int
        double d1 = 80; //ok int -> double
        System.out.println(num);    //97
        System.out.println(d1);     //80.0
    }
}

自动类型转换注意和细节

1.有多种类型的数据混合运算时,系统首先自动将所有数据转换成容量最大的那种数据类型,然后再进行计算。

2.当我们把精度(容量)大的数据类型赋值给精度(容量)小的数据类型时,就会报错,反之就会进行自动类型转换。

3.(byte, short)和char之间不会相互自动转换。

4.byte, short, char 他们三者可以计算,在计算时首先转化为int类型

public class AutoConvertDetail {
    //编写一个main方法
    public static void main(String[] args) {
        //细节1:有多种类型的数据混合运算时,
        //系统首先自动将所有数据转化成容量最大的那种数据类型,然后再进行计算。
        int n1 = 10; //ok
        //float d1 = n1 + 1.1; //错误 n1 + 1.1 => 结果类型是 double
        double d2 = n1 + 1.1; //正确  n1 + 1.1 => 结果类型是 double
        float d3 = n1 + 1.1F; //正确 n1 + 1.1 =>  结果类型是float

        //细节2:当我们把精度(容量)大的数据类型赋值给精度(容量)小的数据类型时,
        //就会报错,反之就会自动类型转换。

        //int n2 = 1.1; // 错误 double -> int

        //细节3:(byte, short) 和 char 之间不会相互自动转换.
        //当把数赋给 byte 时, 先判断该数是否在byte范围内,如果是就可以
        byte b1 = 10; //正确, -128- 127
        int n2 = 1;
        //byte b2 = n2; //错误, 原因: 如果是变量赋值,判段类型
        //
        //char c1 = b1; //错误, 原因byte不能自动转化为char

        //细节4:byte, short, char  他们三者可以计算,在计算时首先转化为int类型

        byte b2 = 1;
        byte b3 = 2;
        short s1 = 1;
        //short s2 = b2 + s1; //错误,b2 + s1 => int
        int s2 = b2 + s1; // 正确
        //byte b4 = b2 + b3 // 错误: b2 + b3 => int
    }

}

5.boolean不参与转换

6.自动提升原则:表达式结果的类型自动提升为操作数中最大的类型

强制类型转换

介绍

自动类型转换的逆过程,将容量大的数据类型转换为容量小的数据类型。使用时要加上强制转换符( ),但可能造成精度降低或溢出,格外要注意。

案例演示:

int n1 = (int)1.9

System.out.println("n1" + n1); //1 造成精度损失

int n2 = 2000;

byte b1 = (byte)n2;

System.out.println("b1= " + b1); // 造成溢出

基本数据类型和String类型的转换

在程序开发中,我们经常需要将基本数据类型转化为String类型,或者将String类型转化为基本数据类型。

基本数据类型转String类型

语法:将基本类型的值 + “ ”即可

案例:

int n1 = 100;

String str1 = n1 + "";

String类型转基本数据类型

语法:通过基本类型的包装类调用parseXX方法即可

案例;

Integer.parseInt("123");

Double.parseDouble("123.1");

Float.parseFloat("123.45");

String str = "123";

int a = Integer.parseInt(str);

获取String字符串中的某个字符

语法:X.charAt(x) //x代表下标(从0开始)

String a = "123";
System.out.println(a.charAt(0));

六、Java中运算符的使用

1.“+”号的使用

(1)当左右两边都是数值型时,则做加法运算

System.out.println(100 + 98); 结果:198

(2)当左右两边有一方为字符串,则做拼接运算

System.out.println("100" + 98); 结果:10098

(3)运算顺序,是从左到右

System.out.println(100 + 3 + "hello"); 结果:103hello

System.out.println("hello" + 100 + 3); 结果:hello1003

2.算术运算符

(1)i++与++i在没有表达式的情况下,等于i = i + 1;

i = 1;

i++; //2

++i; //2

public class ArithmeticOperation {
    public static void main(String [] args){
        //1.面试题1
        int i = 1;
        i = i++;  //规则使用临时变量:(1)temp = i; (2)i = i + 1; (3)i = temp;
        System.out.println(i);  //1
        //2.面试题2
        int i = 1;
        i = ++i;    //规则使用临时变量:(1)i = i + 1; (2)temp = i; (3)i = temp;
        System.out.println(i);  //2
    }
}

3.逻辑运算符

(1).a&b : &叫逻辑与:规则:当a和b同时为true ,则结果为true,否则为false

(2). a&&b : &&叫短路与:规则:当a和b同时为true ,则结果为true,否则为false

(3).a|b:|叫逻辑或,规则:当a和b,有一个为true ,则结果为true,否则为false

4.位运算符

(1)原码、反码、补码(重难点)

  1. 二进制的最高位是符号位:0表示正数,1表示负数(老韩口诀:0->01-> -)·
  2. 正数的原码,反码,补码都一样(三码合一)
  3. 负数的反码=它的原码符号位不变,其它位取反(0->1,1->0)
  4. 负数的补码=它的反码+1,负数的反码=负数的补码-1
  5. 0的反码,补码都是0
  6. java没有无符号数,换言之, java中的数都是有符号的
  7. 在计算机运算的时候,都是以补码的方式来运算的.
  8. 当我们看运算结果的时候,要看他的原码(重点)

(2)位运算符

java中有7个位运算(&、|、^、~、>>、<<和>>>)

√分别是按位与&、按位或|、按位异或^,按位取反~,它们的运算规则是;

按位与&:两位一个为0,一个为1,结果为1,否则为0

按位或 :两位有一个为1,结果为1,否则为0

按位异或:两位一个为0,一个为1,结果为1,否则为0

按位取反~:0->1 ,1->0

比如:2&3=? ~-2 = ? ~2 = ?

public class BitOp {
    //编写一个main方法
    public static void main(String [] args) {
        //计算 2 & 3
        //推导过程
        //1. 先得到 2 的补码 => 2的原码: 00000000 00000000 00000000 00000010
        //         2 的补码: 00000000 00000000 00000000 00000010  正数的原码、补码、反码一样
        //2.       3 的原码: 00000000 00000000 00000000 00000011
        //         3 的补码: 00000000 00000000 00000000 00000011
        //3.    按位&
        //            00000000 00000000 00000000 00000010
        //            00000000 00000000 00000000 00000011
        //            00000000 00000000 00000000 00000010 (运算后的补码)
        //运算后的原码是 00000000 00000000 00000000 00000010 正数的原码、补码、反码一样
        System.out.println(2&3);    //2

        //推导过程
        //1. 先得到 -2 的反码 => -2的原码: 10000000 00000000 00000000 00000010
        //2.         -2 的反码: 11111111 11111111 11111111 11111101
        //3.         -2 的补码: 11111111 11111111 11111111 11111110
        //4.        ~-2 的操作   00000000 00000000 00000000 00000001 运算后的补码
        //5.运算后的原码          00000000 00000000 00000000 00000001 正数的原码、补码、反码一样
        System.out.println(~-2);    //1

        //推导过程
        //1. 先得到 2 的补码 => 2的原码: 00000000 00000000 00000000 00000010
        //2.       2 的补码:  00000000 00000000 00000000 00000010
        //3.         ~2操作:  11111111 11111111 11111111 11111101 运算后的补码 注意是负数
        //4.        2的反码:   11111111 11111111 11111111 11111100
        //5.        2的原码:   10000000 00000000 00000000 00000011
        //6.  运算后的结果 -3
        System.out.println(~2); //-3
    }
}

还有3个位运算符>>、<<和>>>,运算规则:

  1. 算术右移>>:低位溢出,符号位不变,并用符号位补溢出的高位
  2. 算术左移<<符号位不变,低位补0
  3. >>>逻辑右移也叫无符号右移,运算规则是:低位溢出,高位补О
  4. 特别说明:没有<<<符号

举例:

int a = 1 >> 2; //1 => 00000001 => 00000000 本质 1/ 2/ 2 = 0

int c = 1<< 2; //1 => 00000001 => 00000100 本质 1*2*2 = 4

public class BitOperation {
    public static void main(String [] args) {
        System.out.println(1 >> 2); // 0
        System.out.println(1 << 2); //4
        System.out.println(4 << 3); // 4 * 2 * 2 * 2 = 32
        System.out.println(4 >> 3); // 15 / 2/ 2 = 3
    }
}

七、键盘输入语句

介绍

在编程中,需要接受用输入的数据,就可以使用键盘输入语句来获取。需要一个扫描器(对象),就是Scanner。

 
import java.util.Scanner;  //表示把java.util下的Scanner类导入
public class Input {
    //编写一个main方法
    public static void main(String [] args) {
        //Scanner类 表示 简单文本扫描器 ,在java.util包
        //1.引入/导入 Scanner类所在的包
        //2.创建 Scanner 对象, new 创建一个对象,
        //myscanner 就是Scanner类的对象
        //
        Scanner myscanner = new Scanner(System.in);

        System.out.println("请输入名字");
        String name = myscanner.next();   //接收用户输入字符串
        System.out.println("请输入年龄");
        int age = myscanner.nextInt();    //接收用户输入年龄
        System.out.println("请输入薪水");
        double salary = myscanner.nextDouble(); //接收用户输入薪水
        System.out.println("人的信息如下");
        System.out.println("名字:" + name + "\t" + "年龄:" + age + "\t" + "薪水:" + salary );
    }
}
八、控制结构

和C语言差不多,注意要创建Scanner对象。

public class NineTable {
    public static void main(String [] args) {
        for(int i = 1; i <= 9; i++) {
            for(int j = 1; j <= i; j++) {
                System.out.print(i + "*" + j + "=" + i * j + "\t");
            }
            System.out.println();
        }
    }
}

代码需要注意print和println的输出格式
print为一般输出,不能换行输出;
println为换行输出。

public class Stars {
    public static void main(String [] args) {
       /* 1.打印整个金字塔*
        *                   // 第1层 有 1个 *    2 * 1  -1   有4 = (总层数-1)个空格
      * * *                 // 第2层 有 3个 *    2 * 2  -1   有3 = (总层数-2)个空格
    * * * * *               // 第3层 有 5个 *    2 * 3  -1   有2 = (总层数-3)个空格
  * * * * * * *             // 第4层 有 7个*     2 * 4  -1   有1 = (总层数-4)个空格
* * * * * * * * *           // 第5层 有 9个*     2 * 5  -1   有0 = (总层数-5)个空格

          4.打印空心的金字塔
         *                   // 第1层 有 1个 *
       *   *                 // 第2层 有 2个 *
     *       *               // 第3层 有 2个 *
   *           *             // 第4层 有 2个 *
 * * * * * * * * *           // 第5层 有 9个 *

*/
        for(int i = 1; i <= 5; i++) { //i 表示层数
            //在输出*之前, 还有输出 对应空格 = 总层数-当前层
            for(int k = 1; k <= 5 - i; k++) {
                System.out.print(" ");
            }
            //控制打印每层的*个数
            for(int j = 1; j <= 2 * i -1 ; j++) {
                System.out.print("*");
            }
            //每打印完一层的*后,就换行 println本身会换行
            System.out.println("");
        }

        for(int i = 1; i <= 5; i++) { //i 表示层数
            //在输出*之前, 还有输出 对应空格 = 总层数-当前层
            for(int k = 1; k <= 5 - i; k++) {
                System.out.print(" ");
            }
            //控制打印每层的*个数
            for(int j = 1; j <= 2 * i -1 ; j++) {
                //当前行的第一个位置是*,最后一个位置也是*,最后一层全部 *
                if(j == 1 || j == 2 * i -1 || i == 5) {
                    System.out.print("*");
                } else {
                    System.out.print(" ");
                }
            }
            //每打印完一层的*后,就换行 println本身会换行
            System.out.println("");
        }
    }
}

九、数组

1.动态初始化

数组的定义

(1)

语法:数据类型 数组名[] = new 数据类型[大小]

int a[] = new int[5];//创建一个数组,名字a,存放5个int。

(2)

语法:数据类型 数组名[];//先声明,这时候还没有分配空间。

数组名 = new 数据类型[大小];//分配空间。

2.静态初始化

语法:数据类型 数组名[] = {元素值,元素值,...}

int a[] = {2, 4, 5, 6, 7, 8, 9, 23, 54};如果知道数组有多少元素,具体值

相当于动态初始化。

3.数组赋值

(1)引用赋值

数组在默认情况下是引用传递,赋的值是地址, 赋值方式是引用赋值,是一个地址。

public class ArrayAssign {
    public static void main(String[] args) {
        int arr1[] = {1,2,3};
        int arr2[] = arr1; //把 arr1赋给 arr2
        arr2[0] = 10;
        //看看arr1的值
        for(int i = 0; i < 3; i++) {
            System.out.println(arr1[i]); 此时输出结果是 10 、2 、3
        }
    }
}

通过上面代码块和图解可以证明,数组赋值方式是引用赋值。

(2)拷贝赋值

public class ArrayAssign {
    public static void main(String[] args) {
        int arr1[] = {10,20,30};
        //创建一个新的数组arr2,开辟新的数据空间,大小 arr1.length
        int arr2[] = new int[arr1.length];
        int arr2[] = arr1; //把 arr1赋给 arr2
        arr2[0] = 100;
        //看看arr1的值
        for(int i = 0; i < 3; i++) {
            System.out.println(arr1[i]); 此时输出结果是 10 、20 、30
        }
        //看看arr2的值
        for(int i = 0; i < 3; i++) {
            System.out.println(arr2[i]); 此时输出结果是 100、20 、30
        }
    }
}

由以上代码和图解可见,拷贝赋值的数组会开辟一个新的空间,两者之间互不影响。

4.多维数组-二维数组

静态二维数组

语法:数据类型 数组名[] = new 数据类型[大小]

int a[][] = new int[2][3];//创建一个二维数组,名字a,存放2 * 3个int。

动态创建二维数组

public class TwoDimensionalArray {
    public static void main(String [] args) {
        /*看一个需求: 动态创建下面的二维数组,并输出
            i = 0:  1
            i = 1:  2  2
            i = 2:  3  3  3

          一个有三个一维数组,每个一维数组的元素是不一样的。
         */
        int arr[][] = new int[3][];   //创建一个二维数组, 但是只是确定一维数组的个数
        for(int i = 0; i < arr.length; i++) { //遍历arr每个一维数组
            //给每个一维数组开空间 new
            //如果没有给一维数组 new, 那么 arr[i] 就是null
            arr[i] = new int[i + 1];

            //遍历一维数组, 并给一维数组的每个元素复制
            for(int j = 0; j < arr[i].length; j++) {
                arr[i][j] = i + 1; //赋值
            }
        }
        System.out.print("=====arr元素======");
        System.out.println();
        for(int i = 0; i < arr.length; i++) {
            for(int j = 0; j < arr[i].length; j++)
            System.out.print(arr[i][j] + " ");
            System.out.println();
        }
    }
}

杨辉三角

1

1 1

1 2 1

1 3 3 1

1 4 6 4 1

1 5 10 10 5 1

......

规律:

  1. 第一行有 1 个元素,第 n 行有 n 个元素。
  2. 每一行的第一个元素和最后一个元素都是1.
  3. 从第三行开始,对于非第一个元素和最后一个元素的元素的值,arr[i][j] = arr[i-1][j] + arr[i-1][j-1]
public class YangHui {
    //编写一个main方法
    public static void main(String [] args) {

        int yangHui[][] = new int[10][];
        for(int i = 0; i < yangHui.length; i++) {

            //给每个一维数组(行)开辟空间
             yangHui[i] = new int[i + 1];
            //给每个一维数组(行)赋值
             for(int j = 0; j < yangHui[i].length; j++) {
                 //每一行的第一个元素和最后一个元素都是1
                 if(j == 0 || j == yangHui[i].length-1 ) {
                     yangHui[i][j] = 1;
                 } else { //中间的元素
                     yangHui[i][j]  = yangHui[i-1][j] + yangHui[i-1][j-1];
                 }
             }
        }
        //遍历杨辉三角
        for(int i = 0; i < yangHui.length; i++) {
            for (int j = 0; j < yangHui[i].length; j++) {
                System.out.print(yangHui[i][j] + " ");
            }
            System.out.println();
        }
    }
}

程序运行结果

十、排序

冒泡排序

public class BubbleSort {
    public static void main(String [] args) {
        int arr[] = {24, 69, 80, 57, 13};
        int temp = 0;
        for(int i = 0 ; i < 5; i ++) {
            for(int j = 0; j < 5-i-1; j++) {
                if(arr[j] > arr[j + 1]) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        for(int k = 0 ; k < 5; k++) {
            System.out.print(arr[k] + " ");  // 13 24 57 69 80 
        }
    }
}

运用包里面的排序函数

import java.util.Arrays;
public class BubbleSort {
    public static void main(String [] args) {
        int arr[] = {24, 69, 80, 57, 13};
        Arrays.sort(arr);
        for(int k = 0 ; k < 5; k++) {
            System.out.print(arr[k] + " ");  // 13 24 57 69 80 
        }
    }
}

十一、面向对象之类与对象

特征:封装、继承、多态。

1、面向对象简述

面向对象是一种现在最为流行的程序设计方法,几乎现在的所有应用都以面向对象为主了,最早的面向对象的概念实际上是由IBM提出的,在70年代的Smaltalk语言之中进行了应用,后来根据面向对象的设计思路,才形成C++,而由C++产生了Java这门面向对象的编程语言。

但是在面向对象设计之前,广泛采用的是面向过程,面向过程只是针对于自己来解决问题。面向过程的操作是以程序的基本功能实现为主,实现之后就完成了,也不考虑修改的可能性,面向对象,更多的是要进行子模块化的设计,每一个模块都需要单独存在,并且可以被重复利用,所以,面向对象的开发更像是一个具备标准的开发模式。

在面向对象定义之中,也规定了一些基本的特征:

(1)封装:保护内部的操作不被破坏;

(2)继承:在原本的基础之上继续进行扩充;

(3)多态:在一个指定的范围之内进行概念的转换。

对于面向对象的开发来讲也分为三个过程:OOA(面向对象分析)、OOD(面向对象设计)、OOP(面向对象编程)。

2、类与对象的基本概念

类与对象时整个面向对象中最基础的组成单元。

类:是抽象的概念集合,表示的是一个共性的产物,类之中定义的是属性和行为(方法);

对象:对象是一种个性的表示,表示一个独立的个体,每个对象拥有自己独立的属性,依靠属性来区分不同对象。

可以一句话来总结出类和对象的区别:类是对象的模板,对象是类的实例。类只有通过对象才可以使用,而在开发之中应该先产生类,之后再产生对象。类不能直接使用,对象是可以直接使用的。

3、类与对象的定义和使用

在Java中定义类,使用关键字class完成。语法如下:

class 类名称 {
         属性 (变量) ;
         行为 (方法) ;
}

范例:定义一个Person类

class Person {     // 类名称首字母大写
    String name ;
    int age ;
    public void tell() {        // 没有static
          System.out.println("姓名:" + name + ",年龄:" + age) ;
         }
}

类定义完成之后,肯定无法直接使用。如果要使用,必须依靠对象,那么由于类属于引用数据类型,所以对象的产生格式(两种格式)如下:

(1)格式一:声明并实例化对象

类名称 对象名称 = new 类名称 () ;

(2)格式二:先声明对象,然后实例化对象:

类名称 对象名称 = null ;

对象名称 = new 类名称 () ;

引用数据类型与基本数据类型最大的不同在于:引用数据类型需要内存的分配和使用。所以,关键字new的主要功能就是分配内存空间,也就是说,只要使用引用数据类型,就要使用关键字new来分配内存空间。

当一个实例化对象产生之后,可以按照如下的方式进行类的操作:

对象.属性:表示调用类之中的属性;

对象.方法():表示调用类之中的方法。

范例:使用对象操作类

package com.wz.classandobj;

class Person { 
    String name ;
    int age ;
    public void get() {
        System.out.println("姓名:" + name + ",年龄:" + age);
    }
}

public class TestDemo {
        public static void main(String args[]) {
            Person per = new Person() ;// 声明并实例化对象
            per.name = "张三" ;//操作属性内容
            per.age = 30 ;//操作属性内容
            per.get() ;//调用类中的get()方法
        }
}

运行结果:

姓名:张三,年龄:30

以上完成了一个类和对象的操作关系,下面换另外一个操作来观察一下:

package com.wz.classandobj;

class Person { 
    String name ;
    int age ;
    public void get() {
        System.out.println("姓名:" + name + ",年龄:" + age);
    }
}

public class TestDemo {
        public static void main(String args[]) {
            Person per = null;//声明对象
            per = new Person() ;//实例化对象
            per.name = "张三" ;//操作属性内容
            per.age = 30 ;//操作属性内容
            per.get() ;//调用类中的get()方法
        }
}

运行结果:

姓名:张三,年龄:30

那么,问题来了,以上两种不同的实例化方式有什么区别呢?

我们从内存的角度分析。首先,给出两种内存空间的概念:

(1)堆内存:保存对象的属性内容。堆内存需要用new关键字来分配空间;

(2)栈内存:保存的是堆内存的地址(在这里为了分析方便,可以简单理解为栈内存保存的是对象的名字)。

任何情况下,只要看见关键字new,都表示要分配新的堆内存空间,一旦堆内存空间分配了,里面就会有类中定义的属性,并且属性内容都是其对应数据类型的默认值。

于是,上面两种对象实例化对象方式内存表示如下:

两种方式的区别在于①②,第一种声明并实例化的方式实际就是①②组合在一起,而第二种先声明然后实例化是把①和②分步骤来。

另外,如果使用了没有实例化的对象,结果如何?
如下:

运行结果:

Exception in thread "main" java.lang.NullPointerException

at com.wz.classandobj.TestDemo.main(TestDemo.java:15)

此时,程序只声明了Person对象,但并没有实例化Person对象(只有了栈内存,并没有对应的堆内存空间),则程序在编译的时候不会出现任何的错误,但是在执行的时候出现了上面的错误信息。这个错误信息表示的是“NullPointerException(空指向异常)”,这种异常只要是应用数据类型都有可能出现。

4、对象引用传递初步分析

引用传递的精髓:同一块堆内存空间,可以同时被多个栈内存所指向,不同的栈可以修改同一块堆内存的内容。

下面通过若干个程序,以及程序的内存分配图,来进行代码的讲解。

我们来看一个范例:

class Person {     
         String name ;
         int age ;
         public void tell() {        
                   System.out.println("姓名:" + name + ",年龄:" + age) ;
         }
}
public class TestDemo {
         public static void main(String args[]) {
                   Person per1 = new Person() ;         // 声明并实例化对象
                   per1.name = "张三" ;
                   per1.age = 20 ;
                   Person per2 = per1 ;  // 引用传递
                   per2.name = "李四" ;
                   per1.tell() ;
         }
}

对应的内存分配图如下:

再来看另外一个:

class Person {
         String name ;
         int age ;
         public void tell() {
                   System.out.println("姓名:" + name + ",年龄:" + age) ;
         }
}
public class TestDemo {
         public static void main(String args[]) {
                   Person per1 = new Person() ;         // 声明并实例化对象
                   Person per2 = new Person() ;
                   per1.name = "张三" ;
                   per1.age = 20 ;
                   per2.name = "李四" ;
                   per2.age = 30 ;
                   per2 = per1 ;// 引用传递
                   per2.name = "王五" ;
                   per1.tell() ;
         }
}

对应的内存分配图如下:

垃圾:指的是在程序开发之中没有任何对象所指向的一块堆内存空间,这块空间就成为垃圾,所有的垃圾将等待GC(垃圾收集器)不定期的进行回收与空间的释放。

5、方法的重载

Java中的方法重载,在一个类中,可以存在多个方法名相同的方法,但是参数列表不同。同一个类中定义的多个方法之间的关系,满足下列的多个方法互相构成重载:

1.多个方法在同一个类中。
2.多个方法具有相同的方法名。
3.多个方法的参数不相同,类型不同或者数量不同。、

public class text1 {
    public static void main(String[] args) {
        show(10);
        show(10,11);
    }
    public static void show(int a){
        System.out.println(a);
    }
    public static void show(int a ,int b){
        System.out.println(a + " " + b);
    }
}
结果:
10
10 11
public class text1 {
    public static void main(String[] args) {
        show(10);
        show('a');
    }
    public static void show(int a){
        System.out.println(a);
    }
    public static void show(char a){
        System.out.println(a);
    }
}
结果:
10
a
public class text1 {
    public static void main(String[] args) {
        show('a',10);
        show(10,'a');
    }
    public static void show(int b,char a){
        System.out.println(b + " " + a);
    }
    public static void show(char a,int b){
        System.out.println(a + " " + b);
    }
}
结果:
a 10
10 a

6、可变参数

基本概念

java允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。

基本语法

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

public class VarParameter {
    public static void main(String [] args) {
    HspMethod Sum = new HspMethod();
    System.out.println(Sum.sum(1,2,3));

    }
}

class HspMethod {
    public int sum(int... nums) {
        //System.out.println("接受的参数个数= " + nums.length);
        int res = 0;
        for(int i = 0; i < nums.length; i++) {
            res += nums[i];
        }
        return res;
    }
}

注意事项和细节

(1)可变参数可以是0个或者任意个。

(2)可变参数的实参可以是数组。

(3)可变参数的本质就是数组。

(4)可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在最后。

(5)一个形参列表中只能出现一个可变参数。

public class VarParameter {
    public static void main(String [] args) {
    HspMethod Sum = new HspMethod();
    System.out.println(Sum.showScore("小明",90,100));
    }
}

class HspMethod {
    public String showScore(String name, double... score) {
        double totalScore = 0;
        for(int i = 0; i < score.length; i++) {
            totalScore += score[i];
        }
        return name + "的总成绩为:" + totalScore;
    }
}
//小明的总成绩为:190.0

7、构造器

需求:前面我们在创建人类的对象时候,是先把一个对象创建好后,再给他的年龄和姓名属性赋值,如果我们现在要求,在创建人类的对象时,就直接指定这个对象的年龄和姓名,该怎么做?这时就可以使用构造器。

基本语法

[修饰符] 方法名(形参列表) {

方法体;

}

说明:

(1)构造起的修饰符可以默认

(2)构造器没有返回值

(3)方法名和类名字必须一样

(4)参数列表和成员方法一样的规则

(5)构造器的调用系统完成

public class Constructor01 {
    public static void main(String[] args) {
        Person01 p1 = new Person01("smith", 80);
        System.out.println("p1的信息如下");
        System.out.println("p1对象name= " + p1.name);
        System.out.println("p1对象age= " + p1.age);
    }
}

class Person01 {
    String name;
    int age;
    //构造器
    //1. 构造器没有返回值,也不能写void
    //2. 构造器的名称和类Person一样
    //3. (String pName, int pAge) 是构造器形参列表, 规则和成员方法一样。
    public Person01(String pName, int pAge) {
        System.out.println("构造器被调用~ 自动完成对象的属性初始化!");
        name = pName;
        age = pAge;
    }
}
//结果:
//构造器被调用~ 自动完成对象的属性初始化!
//p1的信息如下
//p1对象name= smith
//p1对象age= 80

注意事项和细节要点

  1. 一个类可以定义多个不同的构造器,即构造器重载。比如:我们可以再给Person类定义一个构造器,用来创建对象的时候,只指定人名字,不需要指定年龄。
  2. 构造器名和类名要相同
  3. 构造器没有返回值
  4. 构造器是完成对象的初始化,并不是创建对象。
  5. 在创建对象时,系统自动的调用该类的构造方法
  6. 如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器(也叫默认构造器),比如

Person(){},使用javap指令 反编译看看

7. 一旦定义了自己的构造器,默认的构造器就覆盖了,就不能再使用默认的无参构造器,除非显示的定义一下,即:Person(){}

8、this关键字

this的注意事项和使用细节

  1. this关键字可以用来访问本类的属性、方法、构造器。
  2. this用于区分当前类的属性和局部变量。
  3. 访问成员方法的语法:this.方法名(参数列表);
  4. 访问构造器语法:this(参数列表); 注意只能在构造器中使用(即只能在构造器中访问另外一个构造器,必须放在第一条语句)。
  5. this不能在类定义的外部使用,只能在类定义的方法中使用。
public class ThisDetail {
    public static void main(String[] args) {
    T1 t1 = new T1();
    t1.f2();
    t1.f3();
    }
}

class T1 {
    String name = "小明";
    int num = 10;

    public T1() {
        //
        this("jack", 100);
        System.out.println("T() 构造器");
    }

    public T1(String name, int age) {

        System.out.println("T(String name, int age) 构造器");
    }

    //this关键字可以用来访问本类的属性
    public void f3() {
        String name = "smith";
        //传统方式
        System.out.println("name= " + name + " num= " + num);  //name= smith num= 10
        //也可以使用this访问属性
        System.out.println("name= " + this.name + " num= " + this.num); // name= 小明 num= 10
    }

    //细节: 访问成员方法的语法: this.方法名(参数列表);
    public void f1() {
        System.out.println("f1() 方法...");
    }

    public void f2() {
        System.out.println("f2() 方法...");
        //调用本类中的f1
        //第一种方式
        f1();
        //第二种方式
        this.f1();
    }
}
//结果:
//T(String name, int age) 构造器
//T() 构造器
//f2() 方法...
//f1() 方法...
//f1() 方法...
//name= smith num= 10
//name= 小明 num= 10

9、访问修饰符

4种访问修饰符的访问范围

10、封装

封装介绍

封装(encapsulation)就是把抽象出的数据[属性]和对数据的操作[方法]封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作[方法],才能对数据进行操作。

package com.hspedu.encap;

public class Encapsulation {
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("jack");
        person.setAge(30);
        person.setSalary(30000);
        System.out.println(person.info());
        System.out.println(person.getSalary());

        //如果我们自己使用构造器指定属性
        Person smith = new Person("smith", 2000, 50000);
        System.out.println("=======smith的信息=======");
        System.out.println(smith.info());
    }




}
/*
那么在java中如何实现这种类似的控制呢?
请大家看一个小程序(com.hspedu.encap: Encapsulation01.java),
石能随便查看人的年龄,工资等隐私,并对设置的年龄进行合理的验证。年龄合理就设置,否则给默认年龄,
必须在1-12味年龄,工资不能直接查看,name的长度在2-6字符之间
*/

class Person {
    public String name;
    private int age;
    private double salary;

    //构造器 alt + insert

    public Person(String name, int age, double salary) {
//        this.name = name;
//        this.age = age;
//        this.salary = salary;

        this.setName(name);
        this.setAge(age);
        this.setSalary(salary);

    }

    public Person() {

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        //加入对数据的校验,南当于增加了业务逻辑
        if (name.length() >= 2 && name.length() <= 6) {
            this.name = name;
        } else {
            System.out.println("name的长度不在2-6字符之间,给默认名字");
            this.name = "json";
        }
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age >= 1 && age <= 120) {
            this.age = age;
        } else {
            System.out.println("年龄需要在1-120");
            this.age = 18; //给一个默认年龄
        }
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    public String info() {
        return "信息为 name= " + name + " age= " + age + " 薪水= " + salary;
    }
 }

好处:

(1)只能通过规定的方法访问数据
(2)隐藏类的实例细节,方便修改和实现。

11、继承

继承基本介绍和示意图

继承可以解决代码复用,让我们的编程更加靠近人类思维.当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends来声明继承父类即可。

示意图:

基本语法

class 子类 extends 父类 {

}

1)子类就会自动拥有父类定义的属性和方法。

2)父类又叫超类,基类。

3)子类又叫派生类。

12、supper关键字

基本介绍

super 代表父类的引用,用于访问父类的属性、方法、构造

基本语法

package com.hspedu.super_; 
public class A extends Base{
    //4 个属性 //public int n1 = 100; 
    protected int n2 = 200; 
    int n3 = 300;
    private int n4 = 400; 
    public A() {} 
    public A(String name) {} 
    public A(String name, int age) {} 
    // public void cal() { 
    // System.out.println("A 类的 cal() 方法..."); 
    // }
    public void test100() {
    }
    protected void test200() { }
    void test300() { }
    private void test400() { } 
}
package com.hspedu.super_; 
public class B extends A { 
    public int n1 = 888; 
    //编写测试方法 
    public void test() { 
        //super 的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用 super 去访问爷爷类的成员; 
        // 如果多个基类(上级类)中都有同名的成员,使用 super 访问遵循就近原则。A->B->C 
        System.out.println("super.n1=" + super.n1); 
        super.cal(); 
    }
     //访问父类的属性 , 但不能访问父类的 private 属性 [案例]super.属性名 
    public void hi() { 
        System.out.println(super.n1 + " " + super.n2 + " " + super.n3 ); 
    }
    public void cal() { 
        System.out.println("B 类的 cal() 方法..."); 
    }
    public void sum() { 
        System.out.println("B 类的 sum()"); 
        //希望调用父类-A 的 cal 方法 
        //这时,因为子类 B 没有 cal 方法,因此我可以使用下面三种方式 
        //找 cal 方法时(cal() 和 this.cal()),顺序是: 
        // (1)先找本类,如果有,则调用 
        // (2)如果没有,则找父类(如果有,并可以调用,则调用) 
        // (3)如果父类没有,则继续找父类的父类,整个规则,就是一样的,直到 Object 类 
        // 提示:如果查找方法的过程中,找到了,但是不能访问, 则报错, cannot access 
        // 如果查找方法的过程中,没有找到,则提示方法不存在 
        //cal(); 
        this.cal(); //等价 cal 
        //找 cal 方法(super.call()) 的顺序是直接查找父类,其他的规则一样 
        //super.cal(); 
        //演示访问属性的规则
        //n1 和 this.n1 查找的规则是 
        //(1) 先找本类,如果有,则调用 
        //(2) 如果没有,则找父类(如果有,并可以调用,则调用) 
        //(3) 如果父类没有,则继续找父类的父类,整个规则,就是一样的,直到 Object 类 
        // 提示:如果查找属性的过程中,找到了,但是不能访问, 则报错, cannot access 
        // 如果查找属性的过程中,没有找到,则提示属性不存在 
        System.out.println(n1); 
        System.out.println(this.n1); 
        //找 n1 (super.n1) 的顺序是直接查找父类属性,其他的规则一样 
        System.out.println(super.n1); 
    }
    //访问父类的方法,不能访问父类的 private 方法 super.方法名(参数列表); 
    public void ok() { 
        super.test100(); 
        super.test200(); 
        super.test300(); 
        //super.test400();//不能访问父类 private 方法 
    }
    //访问父类的构造器(这点前面用过):super(参数列表);只能放在构造器的第一句,只能出现一句! 
    public B() { 
        //super(); 
        //super("jack", 10); 
        super("jack"); 
    }
}
package com.hspedu.super_; 
public class Super01 { 
    public static void main(String[] args) { 
        B b = new B();//子类对象 
        //b.sum(); 
        b.test(); 
    } 
}
package com.hspedu.super_; 
public class Base { 
    //父类是 Object 
    public int n1 = 999; 
    public int age = 111; 
    public void cal() { 
        System.out.println("Base 类的 cal() 方法..."); 
    }
    public void eat() { 
        System.out.println("Base 类的 eat()....."); 
    } 
}

13、多态

介绍:面向对象的最后一个特性就是多态,那么什么是多态呢? 多态就是对象的多种形态。

java里面的多态主要变现在两个方面:

13.1 引用多态

父类的引用可以指向本类的对象;

父类的引用可以指向子类的对象;

这两句话是什么意思呢,让我们用代码来体验一下,首先我们创建一个父类Animal和一个子类Dog,在主函数里如下所示:

注意:我们不能使用一个子类的引用来指向父类的对象,如:

这里我们必须深刻理解引用多态的意义,才能更好记忆这种多态的特性。为什么子类的引用不能用来指向父类的对象呢?我在这里通俗给大家讲解一下:就以上面的例子来说,我们能说“狗是一种动物”,但是不能说“动物是一种狗”,狗和动物是父类和子类的继承关系,它们的从属是不能颠倒的。当父类的引用指向子类的对象时,该对象将只是看成一种特殊的父类(里面有重写的方法和属性),反之,一个子类的引用来指向父类的对象是不可行的!!

13.2 方法多态

根据上述创建的两个对象:本类对象和子类对象,同样都是父类的引用,当我们指向不同的对象时,它们调用的方法也是多态的。

创建本类对象时,调用的方法为本类方法;

创建子类对象时,调用的方法为子类重写的方法或者继承的方法;

使用多态的时候要注意:如果我们在子类中编写一个独有的方法(没有继承父类的方法),此时就不能通过父类的引用创建的子类对象来调用该方法!!!

注意: 继承是多态的基础。

十二、引用类型转换

了解了多态的含义后,我们在日常使用多态的特性时经常需要进行引用类型转换。

引用类型转换:

1. 向上类型转换(隐式/自动类型转换),是小类型转换到大类型。

就以上述的父类Animal和一个子类Dog来说明,当父类的引用可以指向子类的对象时,就是向上类型转换。如:

2. 向下类型转换(强制类型转换),是大类型转换到小类型(有风险,可能出现数据溢出)。

将上述代码再加上一行,我们再次将父类转换为子类引用,那么会出现错误,编译器不允许我们直接这么做虽然我们知道这个父类引用指向的就是子类对象,但是编译器认为这种转换是存在风险的如:

那么我们该怎么解决这个问题呢,我们可以在animal前加上(Dog)来强制类型转换。如:

但是如果父类引用没有指向该子类的对象,则不能向下类型转换,虽然编译器不会报错,但是运行的时候程序会出错,如:

其实这就是上面所说的子类的引用指向父类的对象,而强制转换类型也不能转换!!

还有一种情况是父类的引用指向其他子类的对象,则不能通过强制转为该子类的对象。如:

这是因为我们在编译的时候进行了强制类型转换,编译时的类型是我们强制转换的类型,所以编译器不会报错,而当我们运行的时候,程序给animal开辟的是Dog类型的内存空间,这与Cat类型内存空间不匹配,所以无法正常转换。这两种情况出错的本质是一样的,所以我们在使用强制类型转换的时候要特别注意这两种错误!!下面有个更安全的方式来实现向下类型转换。。。。

3. instanceof运算符,来解决引用对象的类型,避免类型转换的安全性问题。

instanceof是Java的一个二元操作符,和==,>,<是同一类东东。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。

我们来使用instanceof运算符来规避上面的错误,代码修改如下:

利用if语句和instanceof运算符来判断两个对象的类型是否一致。

补充说明:在比较一个对象是否和另一个对象属于同一个类实例的时候,我们通常可以采用instanceof和getClass两种方法通过两者是否相等来判断,但是两者在判断上面是有差别的。Instanceof进行类型检查规则是:你属于该类吗?或者你属于该类的派生类吗?而通过getClass获得类型信息采用==来进行检查是否相等的操作是严格的判断,不会存在继承方面的考虑

总结:在写程序的时候,如果要进行类型转换,我们最好使用instanceof运算符来判断它左边的对象是否是它右边的类的实例,再进行强制转换。

十三、抽象类

定义:抽象类前使用abstract关键字修饰,则该类为抽象类。

使用抽象类要注意以下几点:

1. 抽象类是约束子类必须有什么方法,而并不关注子类如何实现这些方法。

2. 抽象类应用场景:

a. 在某些情况下,某个父类只是知道其子类应该包含怎样的方法,但无法准确知道这些子类如何实现这些方法(可实现动态多态)。

b. 从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为子类的模板,从而避免子类设计的随意性。

3. 抽象类定义抽象方法,只有声明,不需要实现。抽象方法没有方法体以分号结束,抽象方法必须用abstract关键字来修饰。如:

4、包含抽象方法的类是抽象类。抽象类中可以包含普通的方法,也可以没有抽象方法。如:

5、抽象类不能直接创建,可以定义引用变量来指向子类对象,来实现抽象方法。以上述的Telephone抽象类为例:

public abstract class Telephone {
    public abstract void call();//抽象方法,方法体以分号结束,只有声明,不需要实现
    public void message(){
        System.out.println("我是抽象类的普通方法");
    }//抽象类中包含普通的方法
}
public class Phone extends Telephone {

    public void call() {//继承抽象类的子类必须重写抽象方法
        // TODO Auto-generated method stub
        System.out.println("我重写了抽象类的方法");
    }

}

以上是Telephone抽象类和子类Phone的定义,下面我们看main函数里:

运行结果(排错之后):

十四、接口

1、概念

接口可以理解为一种特殊的类,由全局常量和公共的抽象方法所组成。也可理解为一个特殊的抽象类,因为它含有抽象方法。

如果说类是一种具体实现体,而接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部数据,也不关心这些类里方法的实现细节,它只规定这些类里必须提供的某些方法。(这里与抽象类相似)

2.接口定义的基本语法

[修饰符] [abstract] interface 接口名 [extends父接口1,2....](多继承){

0…n常量 (public static final)

0…n 抽象方法(public abstract)

}

其中[ ]里的内容表示可选项,可以写也可以不写;接口中的属性都是常量,即使定义时不添加public static final 修饰符,系统也会自动加上;接口中的方法都是抽象方法,即使定义时不添加public abstract修饰符,系统也会自动加上。

3.使用接口

一个类可以实现一个或多个接口,实现接口使用implements关键字。java中一个类只能继承一个父类,是不够灵活的,通过实现多个接口可以补充。

继承父类实现接口的语法为:

[修饰符] class 类名 extends 父类 implements 接口1,接口2...{

类体部分//如果继承了抽象类,需要实现继承的抽象方法;要实现接口中的抽象方法

}

注意:如果要继承父类,继承父类必须在实现接口之前,即extends关键字必须在implements关键字前

补充说明:通常我们在命名一个接口时,经常以I开头,用来区分普通的类。如:IPlayGame

以下我们来补充在上述抽象类中的例子,我们之前已经定义了一个抽象类Telephone和子类Phone,这里我们再创建一个IPlayGame的接口,然后在原来定义的两个类稍作修改,代码如下:

public interface IPlayGame {
    public void paly();//abstract 关键字可以省略,系统会自动加上
    public String name="游戏名字";//static final关键字可以省略,系统会自动加上
}
public class Phone extends Telephone implements IPlayGame{

    public void call() {//继承抽象类的子类必须重写抽象方法
        // TODO Auto-generated method stub
        System.out.println("我重写了抽象类的方法");
    }

    @Override
    public void paly() {
        // TODO Auto-generated method stub
        System.out.println("我重写了接口的方法");
    }

}
public class train {


    public static void main(String[] args) {
        // TODO Auto-generated method stub
        IPlayGame i=new Phone();//用接口的引用指向子类的对象
        i.paly();//调用接口的方法
        System.out.println(i.name);//输出接口的常量



}
}

十五、抽象类和接口的区别

我们在多态的学习过程中认识到抽象类和接口都是实现java多态特性的关键部分,两者都包含抽象方法,只关注方法的声明而不关注方法的具体实现,那么这两者又有什么区别呢??我们在编写java程序的时候又该如何抉择呢?

参考博文:深入理解Java的接口和抽象类 - 大数据从业者FelixZh - 博客园

(1)语法层面上的区别

1.一个类只能继承一个抽象类,而一个类却可以实现多个接口。
2.抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;且必须给其初值,所以实现类中不能重新定义,也不能改变其值;抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值。
3.抽象类中可以有非抽象方法,接口中则不能有非抽象方法。
4.接口可以省略abstract 关键字,抽象类不能。
5.接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;

(2)设计层面上的区别

1)抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。

2)设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

下面看一个网上流传最广泛的例子:门和警报的例子:门都有open( )和close( )两个动作,此时我们可以定义通过抽象类和接口来定义这个抽象概念:

abstract class Door {
    public abstract void open();
    public abstract void close();
}
//或者
interface Door {
    public abstract void open();
    public abstract void close();
}

但是现在如果我们需要门具有报警alarm( )的功能,那么该如何实现?下面提供两种思路:

1)将这三个功能都放在抽象类里面,但是这样一来所有继承于这个抽象类的子类都具备了报警功能,但是有的门并不一定具备报警功能;

2)将这三个功能都放在接口里面,需要用到报警功能的类就需要实现这个接口中的open( )和close( ),也许这个类根本就不具备open( )和close( )这两个功能,比如火灾报警器。

从这里可以看出, Door的open() 、close()和alarm()根本就属于两个不同范畴内的行为,open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。因此最好的解决办法是单独将报警设计为一个接口,包含alarm()行为,Door设计为单独的一个抽象类,包含open和close两种行为。再设计一个报警门继承Door类和实现Alarm接口。

interface Alram {
    void alarm();
}

abstract class Door {
    void open();
    void close();
}

class AlarmDoor extends Door implements Alarm {
    void oepn() {
      //....
    }
    void close() {
      //....
    }
    void alarm() {
      //....
    }
}

十六、递归

一、例题

1、斐波那契数列

public class Fibonacci {
    public static void main(String [] args) {
    F f = new F();
    int bf4 = f.fibonacci(7);
    System.out.println("当n = 7时 对应的斐波那契数 = " + bf4);

    }
}

class F {
    public int fibonacci(int n) {
        if(n >= 1) {
            if (n == 1 || n == 2) {
                return 1;
            } else {
                return fibonacci(n - 1) + fibonacci(n - 2);
            }
        } else {
          System.out.println("请输入 >= 1的数!");
          return -1;
        }
    }
}

2、猴子吃桃子

猴子吃桃子问题,有一堆桃子,猴子第一个天吃了其中的一半,并在多吃一个!

以后每天猴子都吃其中的一半,然后再多吃一个。当到第10天时,想再吃时

发现只有1个桃子。 问题:最初共多少个桃子?

public class Fibonacci {
    public static void main(String [] args) {
    F f = new F();
    int bf4 = f.peach(1);
    System.out.println("桃子的最初总数为: " + bf4);

    }
}

class F {
    //思路分析 逆推
    //1. day = 10 时 有1个桃子
    //2. day = 9 时  有(day10 + 1) * 2 = 4
    //3. day = 8 时  有(day9 + 1) * 2 = 10
    //4. 规律就是 前一天的桃子 = (后一天的桃子 + 1) *2
    //5. 递归
    public int peach(int day) {
        if (day == 10) {
            return 1;
        } else if (day >= 1 && day <=9){
            return (peach(day + 1) + 1) * 2;
        } else {
            System.out.println("day在1-10");
            return -1;
        }
    }
}
//共有1534个桃子


3、迷宫问题

public class Labyrinth {
    public static void main(String [] args) {
        //初始化数组
        int[][] map = new int[8][7];

        //将第一行和最后一行设为1
        for(int i = 0; i < 7; i++) {
            map[0][i] = 1;
            map[7][i] = 1;
        }

        //将第一列和最后一列设为1
        for(int i = 0; i < 8; i++) {
            map[i][0] = 1;
            map[i][6] = 1;
        }

        //将第四行第二三列设为1
        map[3][1] = 1;
        map[3][2] = 1;

        //输出当前地图
        System.out.println("========当前的地区如下所示=======");
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[i].length; j++) {
                System.out.print(map[i][j] + " ");
            }
            System.out.println();
        }
        //使用findWay给老鼠找路
        T t1 = new T();
        t1.findWay(map, 1, 1);
        System.out.println("========找路的情况如下所示=======");

        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[i].length; j++) {
                System.out.print(map[i][j] + " ");
            }
            System.out.println();
        }
    }
}

class T {
    //使用递归回溯的思想来解决老鼠出迷宫
    //思路解读
    //1. findWay方法就是专门来找出迷宫的路径
    //2. 如果找到,就返回true,否则返回false
    //3.map 就是二维数组,即表示迷宫
    //4. i,j就是老鼠的位置,初始化的位置为(1,1)
    //5. 因为我们是递归的找路,所以我先规定 map数组的各个值的含义
    //   0 表示可以走 1 表示障碍物 2 表示可以走 3 表示走过,但是走不通是思路
    //6. 当map[6][5] = 2 就说明找到通路,就可以结束, 否则就继续找
    //7. 先确定老鼠找路策略 下->右->上->左
    public boolean findWay(int[][] map, int i, int j) {
        if (map[6][5] == 2) {//说明已经找到
            return true;
        } else {
            if(map[i][j] == 0) {
                //我们假定可以走通
                map[i][j] = 2;


                if(findWay(map, i + 1, j)) { //先走下
                    return true;
                } else if (findWay(map, i , j + 1)){ //右
                    return true;
                } else if (findWay(map,i - 1, j)) {  //上
                    return true;
                } else if (findWay(map, i , j - 1 )) { //左
                    return true;
                } else {
                    map[i][j] = 3;
                    return false;
                }

            } else {
                return false;
            }
        }
    }
}
========当前的地区如下所示=======
1 1 1 1 1 1 1 
1 0 0 0 0 0 1 
1 0 0 0 0 0 1 
1 1 1 0 0 0 1 
1 0 0 0 0 0 1 
1 0 0 0 0 0 1 
1 0 0 0 0 0 1 
1 1 1 1 1 1 1 
========找路的情况如下所示=======
1 1 1 1 1 1 1 
1 2 0 0 0 0 1 
1 2 2 2 0 0 1 
1 1 1 2 0 0 1 
1 0 0 2 0 0 1 
1 0 0 2 0 0 1 
1 0 0 2 2 2 1 
1 1 1 1 1 1 1 

4.汉诺塔

public class HanoiTower {
    public static void main(String [] args) {
        Tower tower = new Tower();
        tower.move(5, 'A', 'B', 'C');
    }

}


class Tower {
    //方法
    //num 表示要移动的个数 a , b , c 分别表示A塔, B塔, C塔。

    public void move(int num, char a, char b, char c) {
        //如果只有一个盘 num = 1
        if (num == 1) {
            System.out.println(a + "->" + c);
        } else {
            //如果有多个盘,可以看成两个盘, 最下面的和上面的所有盘
            //(1)先移动上面所有的盘到b, 借助c
            move(num - 1, a, c, b);
            //(2)把最下面的这个盘, 移动到 c
            System.out.println(a + "->" + c);
            //(3)再把 b 塔的所有盘,移动到c,借助a
            move(num - 1, b, a, c);
        }
    }
}
运行结果:
A->C
A->B
C->B
A->C
B->A
B->C
A->C
A->B
C->B
C->A
B->A
C->B
A->C
A->B
C->B
A->C
B->A
B->C
A->C
B->A
C->B
C->A
B->A
B->C
A->C
A->B
C->B
A->C
B->A
B->C
A->C
;