4.面向对象1(上)
四、面向对象(上)
1、方法
4.1.1、方法概述
方法是一段可以被重复利用的代码片段
一个方法一般都是一个独立的功能
方法只定义不去调用是不会执行的,方法要执行的话肯定是有一个位置调用了这个方法
【示例1】不使用方法
package com.java.method.gaishu;
/*
在这个程序当中不使用方法,分析程序这样写有什么问题
代码复用性差。
相同的代码写了一遍又一遍,这种设计是不合理的。
怎么提高代码的复用性呢?
使用方法(Method)
*/
public class MethodTest01{
public static void main(String[] args){
// 需求1:请编写程序计算100和200的和。
int a = 100;
int b = 200;
int c = a + b;
System.out.println(a + "+" + b + "=" + c);
// 需求2:请编写程序计算666和888的和。
int i = 666;
int j = 888;
int m = i + j;
System.out.println(i + "+" + j + "=" + m);
// 需求3:请编写程序计算123和456的和。
int x = 123;
int y = 456;
int z = x + y;
System.out.println(x + "+" + y + "=" + z);
}
}
【示例2】使用方法
package com.java.method.gaishu;
/*
方法本质上就是一段可以被重复利用的代码片段。
并且每一个方法都是一个独立的封装好的功能。
方法只定义,不去调用,是不会执行的。
方法要执行的话,肯定是有一个位置调用了这个方法。
*/
public class MethodTest02{
// 入口
public static void main(String[] args){
// 调用sum方法,完成求和功能。
// 请编写程序计算100和200的和。
sum(100, 200); // 方法的调用。
// 请编写程序计算666和888的和。
sum(666, 888); // 方法的调用。
// 请编写程序计算123和456的和。
sum(123, 456); // 方法的调用。
}
// 单独定义一个方法,这个方法完成一个具体的功能。求和功能。
// 方法的定义。
public static void sum(int a, int b){
int c = a + b;
System.out.println(a + "+" + b + "=" + c);
}
}
4.1.2、方法定义
语法
[修饰符] 返回值类型 方法名(形参列表){
方法体代码(需要执行的功能代码)
return 返回值;
}

注意事项
-
方法的修饰符
主要用来控制方法的访问权限;
权限修饰符可以是private、public、protected、default中的任意一个,也可以不写
-
方法的返回值类型
-
有返回值
可以是java语言中任何一种数据类型。包括基本数据类型、引用数据类型
例如:byte short int long float double boolean char String…
返回值类型是int表示方法结束的时候会返回一个整数给调用者
返回值类型是String表示方法结束的时候会返回一个字符串给调用者
-
无返回值
如果方法执行结束的时候没有返回任何数据给调用者,返回值类型写void
切记不能空着不写
-
-
方法名
只要是合法的标识符即可,方法名最好是反映了这个方法所实现的功能
首字母小写,后面每个单词首字母大写
一般方法都是一个行为,所以方法名一般都是动词
-
形参列表
形式参数列表一般简称为"形参"
形参个数是"0-N个",多个的话使用逗号隔开,例如(int a, double b, long c)
形参是局部变量
形参中起决定性作用的是"形参的类型",形参名随意
-
方法体
任何一个方法都有一个方法体,方法体用大括号括起来
在大括号中编写"java语句;“,并且方法体中的代码有执行顺序"自上而下”
-
方法的返回值
-
返回值不是void
当返回值类型不是void的时候方法在结束的时候必须使用"return 值;"语句来完成数据的返回
-
当返回值类型是void
不能编写"return 值;" 这样的语句
可以编写"return;"语句,主要是用来终止方法的执行
也可以不编写"return;",这个要看具体的业务
-
-
return语句
return语句有两种写法:
- 第一种:return 值;
- 第二种:return;
只要return语句执行方法必然结束
return语句下面不能编写代码,因为永远执行不到,属于无效的代码
-
返回值接收
当调用一个返回值类型不是void的方法时,方法结束的时候会返回值,这个值可以采用变量接收
变量的类型一定要和返回值类型一致、或者能够自动类型转换、或者强制类型转换
方法执行结束有返回值,但是对于调用者来说可以选择接收,也可以选择不接收
【示例】
package com.java.method.dingyi;
/*
方法的定义
1. 方法的定义
1.1 语法格式:
[修饰符列表] 返回值类型 方法名(形式参数列表){
方法体;
}
1.2 修饰符列表:设置方法的访问权限
1.3 返回值类型
可以是java语言中任何一种数据类型。包括基本数据类型,引用数据类型。
例如:byte short int long float double boolean char String....
如果方法执行结束的时候没有返回任何数据给调用者,返回值类型写:void。
切记:不能空着不写。
返回值类型是int表示:方法结束的时候会返回一个整数给调用者。
返回值类型是String表示:方法结束的时候会返回一个字符串给调用者。
返回值类型是void表示:方法结束的时候不返回任何数据给调用者。
1.4 当返回值类型不是void的时候,方法在结束的时候必须使用"return 值;"语句来完成数据的返回。
1.5 return语句有两种写法:
第一种:return 值;
第二种:return;
不管是哪一种,只要return语句执行,方法必然结束。
1.6 当返回值类型是void的时候:
不能编写"return 值;" 这样的语句。
但是可以编写"return;"语句,主要是用来终止方法的执行。也可以不编写"return;",这个要看具体的业务。
1.7 当调用一个返回值类型不是void的方法时,方法结束的时候会返回值,这个值可以采用变量接收。注意变量的类型。
变量的类型一定要和返回值类型一致。或者能够自动类型转换,或者强制类型转换。
1.8 方法执行结束有返回值,但是对于调用者来说,可以选择接收,也可以选择不接收。
1.9 方法名:只要是合法的标识符即可。首字母小写,后面每个单词首字母大写。(一般方法都是一个行为,所以方法名一般都是动词。)
方法名最好是反映了这个方法所实现的功能。
deleteUser
saveUser
login
logout
....
1.10 形式参数列表一般简称为:形参。形参个数是:0-N个。多个的话,使用逗号隔开。
形参是局部变量。形参中起决定性作用的是:形参的类型。形参名随意。
1.11 方法体:任何一个方法都有一个方法体。方法体用大括号括起来:
在大括号中编写"java语句;"
并且方法体中的代码有执行顺序,遵循自上而下。
*/
public class MethodTest01 {
public static void main(String[] args) {
}
// 报错原因:因为返回值类型是int,那么结束时就必须要返回一个整数。
// 但是没有写 return 返回语句来完成数据的返回
/*
public static int sum(int a, int b){
}
*/
// 解决错误:添加返回语句。
public static int sum(int a, int b) {
int c = a + b;
return c;
}
public static boolean m1() {
// 不兼容的类型: int无法转换为boolean
//return 1;
// 修改
return true;
}
// 缺少返回语句
/*
public static String m2(){
}
*/
// 修改
public static String m2() {
return "say hello";
}
// 编译报错:方法定义的返回值与实际的返回值类型不一致
/*
public static void m4(){
return 100;
}
*/
/*
当方法的返回值是void的时候可以使用return返回
*/
public static void m5() {
return;
}
// 当方法定义了返回值时如果不使用return返回会报错
/*
public static int m6(){
return;
}
*/
}
4.1.3、方法调用
方法没有被调用的时候在方法区中的字节码文件中存放
方法被调用的时候进入到栈内存中运行
方法如果只定义不调用是不会分配内存空间(从Java8开始方法的字节码指令存储在元空间metaspace当中,元空间使用的是本地内存)
方法调用的瞬间会在JVM的栈内存当中分配活动场所,此时发生压栈动作
方法一旦结束,给该方法分配的内存空间就会释放,此时发生弹栈动作
4.1.3.1、方法调用示例
package com.java.method.diaoyong;
/*
1. 方法的调用
当这个方法修饰符列表有static关键字的时候:
调用的语法格式:
类名.方法名(实际参数列表);
实际参数列表简称为:实参。
实参和形参列表必须一一对应:
类型要一一对应。
个数要一一对应。
*/
public class MethodDemo0 {
public static void main(String[] args) {
// 调用方法
int result = add(1, 2);
System.out.println(result);
// 这个过程就存在自动类型转换。
long result2 = add(200, 300);
System.out.println(result2);
// concat方法结束后返回的是String值,必须使用String类型变量接收这个值。
String content = concat("hello,", "jack");
System.out.println(content);
add(1111, 2222);
// 错误
//add(false, 1);
// 错误
//add(1);
// 用完整语法调用一次方法。
String retValue = MethodDemo0.concat("abc", "def");
System.out.println(retValue);
}
// 求和的一个方法。
public static int add(int a, int b) {
System.out.println("add..........");
//int z = x + y;
//return z;
return a + b;
}
public static String concat(String x, String y) {
//String z = x + y;
//return z;
return x + y;
}
// 报错原因:因为返回值类型是int,那么结束时就必须要返回一个整数。但是没有写 return 返回语句来完成数据的返回
/*
public static int sum(int a, int b){
}
*/
// 解决错误:添加了返回语句。
public static int sum(int a, int b) {
//int c = a + b;
//return c;
return 0;
}
public static boolean m1() {
// 不兼容的类型: int无法转换为boolean
//return 1;
// 修改
return true;
}
// 缺少返回语句
/*
public static String m2(){
}
*/
// 修改
public static String m2() {
return "say hello";
}
public static void m3() {
}
// 编译报错:言行不一。
/*
public static void m4(){
return 100;
}
*/
public static void m5() {
return;
}
// 缺少返回值
/*
public static int m6(){
return;
}
*/
}
4.1.3.2、方法调用内存图
(1)、示例代码
package com.java.method.diaoyong;
public class MethodDemo2 {
public static void main(String[] args) {
study();
}
public static void sleep() {
System.out.println("睡觉");
}
public static void eat() {
System.out.println("吃饭");
}
public static void study() {
eat();
System.out.println("学习");
sleep();
}
}
(2)、内存图
初始内存结构

加载类的字节码文件

执行main()方法,在栈内存开辟main()方法的栈空间

执行main()方法的第一行代码

开辟study()方法的栈空间

执行study()方法的第一行代码

这里调用了eat()方法,先开辟eat()方法的栈空间

执行eat()方法的第一行代码

输出"吃饭"

eat()方法执行完毕,弹出栈空间

继续执行study()方法的第二行代码

输出"学习"

继续执行study()方法的第三行代码

这里调用了sleep()方法,先开辟sleep()方法的栈空间

执行sleep()方法的第一行代码

输出"睡觉"

sleep()方法执行完毕,弹出栈空间

study()方法执行完毕,弹出栈空间

main()方法执行完毕,弹出栈空间

4.1.4、方法参数
4.1.4.1、形参和实参
调用方法时可以给该方法传递一个或多个值
参数可以使用任何数据类型(包括基本数据类型、引用类型)
-
实参
传给方法的值叫作实参
-
形参
在方法内部接收实参的变量叫作形参
形参的声明语法与变量的声明语法一样
形参只在方法内部有效
参数传递过程中有两项规则
- 对于基本数据类型(以及String类型)来说形参的操作不会影响实际参数
- 对于引用数据类型(除了String类型)来说形参的操作会影响实际参数
(1)、实参
方法调用时实际传递的参数(实际的值,相当于赋值)
(2)、形参
方法的参数定义,是临时变量
【示例代码】
package com.java.method.methodParam;
public class ParamDemo1 {
public static void main(String[] args) {
int a = foo(1, 2);
System.out.println(a);
}
public static int foo(int a, int b) {
int c = a + b;
return c;
}
}
4.1.4.2、不定长参数
声明方法时如果有若干个相同类型的参数则可以定义为不定长参数
该类型的参数声明如下:
权限修饰符 返回值类型 方法名(参数类型… 参数名)
可变参数的作用是传递参数非常灵活,可以不传参数、传递1个或者多个参数,也可以传递一个数组
注意
- 参数类型和参数名之间是三个点而不是其他数量或省略号
- 可变参数在方法内部本质上就是一个数组
- 一个形参列表中可变参数只能有一个
- 可变参数必须放在形参列表的最后面
【示例1】
package com.java.method.methodParam;
import java.util.Arrays;
public class ParamDemo2 {
public static void main(String[] args) {
sum();//不传参数
sum(10);//传输一个参数
sum(10, 20, 30);//传输多个参数
sum(new int[]{10, 20, 30, 40, 50});//传输一个数组
}
public static void sum(int... nums) {
//注意:可变参数在方法内部其实就是一个数组,nums
System.out.println("元素个数:" + nums.length);
System.out.println("元素内容:" + Arrays.toString(nums));
}
}
【示例2】
对20、30、40、50、60求和就可以使用参数为不定长参数的成员方法
成员方法add(int… x)的代码如下
package com.java.method.methodParam;
public class ParamDemo3 {
public static void main(String[] args) {
int a = add(20, 30, 40, 50, 60);
System.out.println(a);
}
public static int add(int... x) { // 定义add方法,并指定不定长参数的类型为int
int result = 0; // 记录运算结果
for (int i = 0; i < x.length; i++) { // 遍历参数
result += x[i]; // 执行相加操作
}
return result; // 返回运算结果
}
}
4.1.4.3、参数传递内存图
Java参数的传递都是值传递
(1)、无参数传递内存图
【示例代码】
package com.java.method.methodParam;
public class MethodDemo1 {
public static void main(String[] args) {
System.out.println("开始");
getMax();
System.out.println("结束");
}
public static void getMax() {
int num1 = 10;
int num2 = 20;
int max = num1 > num2 ? num1 : num2;
System.out.println(max);
}
}
内存图
初始内存结构

加载类的字节码

执行main()方法,首先在栈内存中开辟main方法的空间

执行main()方法的第一行代码

输出"开始"

执行main()方法中的第二行代码"getMax()"方法

在栈内存中开辟getMax()方法的空间

执行getMax()方法的的第一行代码

执行getMax()方法的第二行代码

执行getMax()方法的第三行代码

执行getMax()方法的第四行代码

输出getMax()方法的结果

getMax()方法执行完毕,将getMax()方法的栈空间弹出栈内存

继续执行main()方法的第三行代码

输出"结束"

main()方法执行完毕,弹出栈空间

整个方法执行结束

(2)、基本类型参数传递内存图
【示例】
package com.java.method.methodParam;
public class MethodDemo2 {
public static void main(String[] args) {
getMax(10, 20);
System.out.println("-------");
getMax(30, 40);
}
public static void getMax(int num1, int num2) {
int max = num1 > num2 ? num1 : num2;
System.out.println(max);
}
}
内存图解
初始内存

加载字节码文件

执行main()方法,先开辟main()方法的栈内存空间

执行main()方法的第一行代码

调用了getMax()方法,开辟该方法的栈内存空间

该方法有参数,放入局部变量表中对应的槽位

将参数10赋值给num1

将参数20赋值给num2

执行getMax()方法的第一行代码

执行getMax()方法的第二行代码

输出getMax()方法的结果

getMax()方法执行完毕,弹出getMax()方法的栈空间

继续执行main()方法的第二行代码

输出分割线

继续执行main()方法的第三行代码

开辟getMax()方法的栈空间

将形参放入局部变量表对应的槽位

将实参赋值给形参

执行getMax()方法的第一行代码

执行getMax()方法的第二行代码

输出getMax()方法的结果

getMax()方法执行完毕,弹出栈空间

main()方法执行完毕,弹出栈空间

4.1.5、重载与重写
4.1.5.1、方法重载
(1)、什么是重载
方法的重载就是在同一个类中允许同时存在多个同名方法,只要这些方法的形参列表不同(参数个数不同、参数类型不同、参数顺序不同)即可
方法重载只与"参数类型和个数相关"(即"同名不同参")
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表
最常用的地方就是构造器的重载
调用重载方法的时候会通过参数的不同来区分调用的是哪个方法
使用重载的好处是对于相似功能的业务场景可读性好,方法名相同是提示同一类型的功能,通过形参不同实现差异化的选择
方法重载机制属于编译阶段的功能,在编译阶段已经完成了方法的绑定、确定了要调用哪个方法了
(2)、重载的特点
- 重载发生在一个类中
- 方法名相同
- 参数列表不同
(3)、使用示例
【示例1】
package com.java.method.overLoad;
/*
当前代码先不使用方法重载,分析一下这样写的代码有什么缺点
缺点一:代码不美观。
缺点二:不方便调用,程序员需要记忆更多的方法名。
*/
public class OverloadTest01{
public static void main(String[] args){
// 调用方法
sumInt(10, 20);
sumLong(10L, 20L);
sumDouble(3.0, 2.0);
}
public static void sumInt(int a, int b){
int c = a + b;
System.out.println(a + "+" + b + "=" + c);
}
public static void sumLong(long a, long b){
long c = a + b;
System.out.println(a + "+" + b + "=" + c);
}
public static void sumDouble(double a, double b){
double c = a + b;
System.out.println(a + "+" + b + "=" + c);
}
}
【示例2】
package com.java.method.overLoad;
/*
使用java语言中的方法重载机制。分析程序优点
在java语言中允许在一个类中定义多个方法,这些方法的名字可以一致。
优点1:代码美观。
优点2:方便调用了,程序员需要记忆的方法名少了。
*/
public class OverloadTest02{
public static void main(String[] args){
// 调用方法
sum(10, 20);
sum(10L, 20L);
sum(3.0, 2.0);
}
public static void sum(int a, int b){
int c = a + b;
System.out.println(a + "+" + b + "=" + c);
}
public static void sum(long a, long b){
long c = a + b;
System.out.println(a + "+" + b + "=" + c);
}
public static void sum(double a, double b){
double c = a + b;
System.out.println(a + "+" + b + "=" + c);
}
}
【示例3】
package com.java.method.overLoad;
/*
当一个程序满足怎样的条件时,代码就构成了方法重载呢?
条件1:在同一个类中。
条件2:方法名一致。
条件3:形式参数列表不同:
类型不同算不同
顺序不同算不同
个数不同算不同
方法重载是编译阶段的机制还是运行阶段的机制?
方法重载机制是编译阶段的机制。
在编译阶段已经完成了方法的绑定。
在编译阶段已经确定了要调用哪个方法了。
什么情况下考虑使用方法重载
在以后的开发中,在一个类中,如果两个方法的功能相似,建议将方法名定义为同一个名字。
此时就使用了方法重载机制。
*/
public class OverloadTest03{
public static void main(String[] args){
m1();
m1("abc");
m2(10, 20);
m2(10L, 20L);
m3("x", 10);
m3(10, "x");
}
// 形参的个数不同
public static void m1(){
System.out.println("m1()");
}
public static void m1(String s){
System.out.println("m1(String s)");
}
// 形参类型不同
public static void m2(int a, int b){
System.out.println("m2(int a, int b)");
}
public static void m2(long a, long b){
System.out.println("m2(long a, long b)");
}
// 形参顺序不同
public static void m3(String s, int a){
System.out.println("m3(String s, int a)");
}
public static void m3(int a, String s){
System.out.println("m3(int a, String s)");
}
// 以下这两个方法没有构成方法重载,属于方法重复定义了。语法错误,编译器报错。
/*
public static void doSome(int a, int b){
}
public static void doSome(int x, int y){
}
*/
}
4.1.5.1、方法重写
在继承体系中子类可以继承到父类的方法
当从父类中继承过来的方法无法满足子类的业务需求时就需要采用方法的重写
方法重写又称方法覆盖

(1)、重写的条件
条件1:方法覆盖发生在具有继承关系的父子类之间
条件2:具有相同的方法名(必须严格一样)
条件3:具有相同的形参列表(必须严格一样)
条件4:具有相同的返回值类型(可以是子类型)
即外壳不变,核心重写!
(2)、重写的特点
特点为"三同两小一大"
-
三同
方法名称、参数类型、参数个数全部相同
-
两小
-
子方法不能抛出更新更广的异常
-
子类的方法返回类型必须和所覆盖的方法相同(如果返回值类型是引用数据类型,那么这个返回值类型可以是原类型的子类型)
-
-
一大
访问权限不能变低,可以变高
(3)、重写的限制
- 私有的方法以及构造方法不能继承所以他们不存在方法覆盖
- 方法覆盖针对的是实例方法,和静态方法无关
- 方法覆盖针对的是实例方法,和实例变量没有关系
(4)、使用示例
package com.java.method.repeat;
/**
* 动物类
*/
public class Animal {
/**
* 吃的行为
*/
public void eat() {
System.out.println("动物在吃东西");
}
/**
* 移动的行为
*/
public void move() {
System.out.println("动物在移动");
}
public Object getObj(long a, String b) {
return null;
}
}
class Bird extends Animal {
/**
* Bird对继承过来的move()方法不满意。
* Bird类有权利将move()方法进行重写/覆盖。
*/
@Override
public void move() {
System.out.println("鸟儿在飞翔!");
}
@Override
public String getObj(long a, String b) {
return "";
}
}
/**
* 方法覆盖/override/方法重写/overwrite
* 1. 什么时候考虑使用方法重写?
* 当从父类中继承过来的方法,无法满足子类的业务需求时。
* <p>
* 2. 当满足什么条件的时候,构成方法重写?
* 条件1:方法覆盖发生在具有继承关系的父子类之间。
* 条件2:具有相同的方法名(必须严格一样)
* 条件3:具有相同的形参列表(必须严格一样)
* 条件4:具有相同的返回值类型(可以是子类型)
* <p>
* 3. 关于方法覆盖的细节:
* 3.1 当子类将父类方法覆盖之后,将来子类对象调用方法的时候,一定会执行重写之后的方法。
* 3.2 在java语言中,有一个注解,这个注解可以在编译阶段检查这个方法是否是重写了父类的方法。
*
* @Override注解是JDK5引入,用来标注方法,被标注的方法必须是重写父类的方法,如果不是重写的方法,编译器会报错。
* @Override注解只在编译阶段有用,和运行期无关。 3.3 如果返回值类型是引用数据类型,那么这个返回值类型可以是原类型的子类型
* 3.4 访问权限不能变低,可以变高。
* 3.5 抛出异常不能变多,可以变少。
* 3.6 私有的方法,以及构造方法不能继承,因此他们不存在方法覆盖。
* 3.7 方法覆盖针对的是实例方法。和静态方法无关。
* 3.8 方法覆盖针对的是实例方法。和实例变量没有关系。
*/
class OverrideTest01 {
public static void main(String[] args) {
// 创建鸟儿对象
Bird b = new Bird();
// 调用方法
b.eat();
b.move();
}
}
4.1.6、方法递归
4.1.6.1、什么是方法递归
方法直接调用自己或者间接调用自己的形式称为方法递归
递归的形式
-
直接递归
方法自己调用自己
-
间接递归
方法调用其它方法,其他方法又回调方法自己
递归算法三要素
- 递归的公式:f(n)=f(n-1)*n
- 递归的终结点:f(1)
- 递归的方向必须走向终结点
注意事项
- 递归必须要有结束条件(有结束条件也可能发生内存溢出)
- 递归和循环都能完成的话优先选择循环(递归更耗费内存)
- 递归如果没有控制好终止会出现递归死循环导致栈内存溢出现象
【示例】
package com.java.method.digui;
/*
方法的递归调用:
1. 什么是方法的递归调用?
方法自己调用自己。
2. 递归调用如果没有结束条件的话,会出现栈内存溢出错误:
java.lang.StackOverflowError
3. 所有的递归调用必须要有结束条件。
4. 在实际开发中,使用递归调用的时候,即使有的时候,结束条件是存在的,并且结束条件也是合法的。
但仍然会发生栈内存溢出错误,这可能是因为递归太深,栈内存不够了导致的。所以递归调用一般是不建议
使用的。只有在不使用递归调用时这个问题解决不了的情况下,才建议使用递归调用。
原则:能用循环尽量使用循环。
因为递归调用太耗费栈内存。
5. 在实际开发中,如果因为递归调用发生了栈内存溢出错误该怎么办
首先可以调整栈内存的大小。扩大栈内存。
如果扩大之后,运行一段时间还是出现了栈内存溢出错误。
可能是因为递归结束条件不对。需要进行代码的修改。
*/
public class RecursionTest01{
public static void main(String[] args){
recursion();
}
public static void recursion(){
System.out.println("recursion begin");
// 方法的递归调用
recursion();
System.out.println("recursion over");
}
/*
public static void recursion(){
System.out.println("recursion begin");
// 方法的递归调用
recursion();
System.out.println("recursion over");
}
public static void recursion(){
System.out.println("recursion begin");
// 方法的递归调用
recursion();
System.out.println("recursion over");
}
public static void recursion(){
System.out.println("recursion begin");
// 方法的递归调用
recursion();
System.out.println("recursion over");
}
public static void recursion(){
System.out.println("recursion begin");
// 方法的递归调用
recursion();
System.out.println("recursion over");
}
public static void recursion(){
return;
}
*/
//........
}
4.1.6.2、递归内存图
【示例】计算1-n的阶乘
需求:计算1-n的阶乘,使用递归思想解决
分析
假如认为存在一个公式是f(n)=1*2*3*4…*(n-1)*n
那么公式等价形式就是:f(n)=f(n-1)*n
如果求的是1-5的阶乘


(1)、示例代码
package com.java.method.digui;
public class RecursionDemo1 {
public static void main(String[] args) {
int result = jc(5);
System.out.println(result);
}
public static int jc(int num) {
if (num == 1) {
return 1;
} else {
return num * jc(num - 1);
}
}
}
(2)、内存图
初始栈内存

执行main方法,将main方法压栈

执行第一行代码

第一行代码调用了jc方法,继续在栈空间的栈顶开辟jc方法的内存空间

此jc方法栈的栈空间中的局部变量是5

根据条件执行jc方法中的"num*jc(num-1);"代码

又调用了jc方法,再开辟一个jc方法的栈空间

此jc方法栈的栈空间中的局部变量是4

根据条件执行jc方法中的"num*jc(num-1);"代码

又调用了jc方法,继续开辟jc方法的栈空间

此jc方法栈的栈空间中的局部变量是3

根据条件执行jc方法中的"num*jc(num-1);"代码

继续开辟jc方法的栈空间

此jc方法栈的栈空间中的局部变量是2

根据条件执行jc方法中的"num*jc(num-1);"代码

继续开辟jc方法的栈空间

此jc方法栈的栈空间中的局部变量是1

根据条件执行"return 1;"代码

参数为1的jc方法执行完毕,返回值为1

方法返回到参数为2的jc方法的栈空间,此时可以得到"jc(num-1);" 的值

计算出"num*jc(num-1);“的值为"2*1”

参数为2的jc方法执行完毕

方法返回到参数为3的jc方法的栈空间,此时可以得到"jc(num-1);"的值

计算出"num*jc(num-1);“的值为"3*2*1”

参数为3的方法执行完毕

方法返回到参数为4的jc方法的栈空间,此时可以得到"jc(num-1);"的值

计算出"num*jc(num-1);“的值为"4*3*2*1”

参数为4的方法执行完毕

方法返回到参数为5的jc方法的栈空间,此时可以得到"jc(num-1);"的值

计算出"num*jc(num-1);“的值为"5*4*3*2*1”

继续执行main方法后面的输出语句,main方法执行完毕

4.1.6.3、常见笔试题
(1)、N的求和
package com.java.method.mianshi;
// 使用递归调用,计算1+2+3+4+...n的和。
// 思路:5 + 4 + 3 + 2 + 1
public class RecursionTest03{
public static void main(String[] args){
int n = 5;
int result = sum(n);
System.out.println("result = " + result);
}
// 计算1到n的和。
public static int sum(int n){
if(n == 1){
return 1;
}
return n + sum(n - 1);
}
}

(2)、N的阶乘
使用递归计算n的阶乘
package com.java.method.mianshi;
// 使用递归计算n的阶乘
// 5的阶乘:5 * 4 * 3 * 2 * 1
public class RecursionTest04{
public static void main(String[] args){
int n = 5;
int result = jieCheng(n);
System.out.println("result = " + result);
}
public static int jieCheng(int n){
if(n == 1){
return 1;
}
return n * jieCheng(n - 1);
}
}
(3)、不死神兔
有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问第二十个月的兔子对数为多少
package com.java.method.mianshi;
/*
假如有一对兔子,从出生后第3个月起每个月都生一对兔子,
小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,
请问第n个月后的兔子有多少对
*/
/*
月份 兔子数量(多少对) 最终多少对
------------------------------------------------------------------------------------------------------------
1 1(新生1) 1
2 1(新生2) 1
3 1(成熟) + 1(新生1) 2
4 1(成熟) + 1(新生1) + 1(新生2) 3
5 1(成熟) + 1(新生1) + 1(新生2) + 1(成熟) + 1(新生1) 5
6 1(成熟) + 1(新生1) + 1(新生2) + 1(成熟) + 1(新生1) + 1(成熟) + 1(新生1) + 1(新生2) 8
当月的兔子总数 = 上个月兔子总数 + 本月所有新生的兔子
当月的兔子总数 = 上个月兔子总数 + 上上个月兔子的总数
1
1
2
3
5
8
13
21
34
55
89
144 12月份的时候兔子的总对数!!!
规律:当前数字 = 前两个相邻的数字之和。
这种数字被称为斐波那契数字。
斐波那契数字组合起来的叫做:斐波那契数列。
*/
public class Rabbit{
public static void main(String[] args){
// n变量,表示第n个月
int n = 6;
int total = rabbit(n);
System.out.println(n + "月的时候兔子总对数:" + total); // 13
}
// 这个rabbit方法就是用来计算第n个月的时候兔子总对数。
public static int rabbit(int n){
if(n == 1 || n == 2){
// 递归的结束条件
return 1;
}
return rabbit(n - 1) + rabbit(n - 2);
}
}
(4)、猴子吃桃
猴子第一天摘下若干个桃子
当即吃了一半,还不过瘾,又多吃了一个
第二天早上又将剩下的桃子吃掉一半,又多吃了一个
以后每天早上都吃了前一天剩的一半零一个
到第10天早上想再吃时,见只剩下一个桃子了
求第一天共摘了多少桃子?
package com.java.method.mianshi;
/**
* 猴子第一天摘下若干个桃子,当即吃了一半,还不过瘾,又吃了一个,
* 第二天早上又将剩下的桃子吃了一半,又多吃了一个,以后每天早上
* 都是吃了前一天剩下的一半零一个,到第10天早上再吃的时候,发
* 现只剩下一个桃子了,问一共多少个桃子。
*
* 假设桃子的总数量是:count
* 第1天:count = count / 2 - 1
* 第2天:count = count / 2 - 1
* 第3天:count = count / 2 - 1
* 第4天:count = count / 2 - 1
* 第5天:count = count / 2 - 1
* 第6天:count = count / 2 - 1
* 第7天:count = count / 2 - 1
* 第8天:count = count / 2 - 1
* 第9天:count = count / 2 - 1
* 第10天:1
*/
public class Test01 {
public static void main(String[] args) {
// 第10天的桃子数量
int count = 1;
for (int i = 0; i < 9; i++) {
count = (count + 1) * 2;
}
System.out.println("桃子总数:" + count);
}
}
(5)、百僧百膜
100个和尚吃了100个馒头,100和尚有大和尚和小和尚,一个大和尚能吃3馒头,三个小和尚吃1个馒头,问大和尚和小和尚有多少个?
package com.java.method.mianshi;
/**
* 100个和尚吃了100个馒头,100和尚有大和尚和小和尚,一个大和尚能吃3馒头,三个小和尚吃1个馒头,问大和尚和小和尚有多少个?
*/
public class Test02 {
public static void main(String[] args) {
// 大和尚最多是多少个:33
for (int i = 0; i <= 33; i++) { // i是大和尚数量
// 小和尚数量
int j = 100 - i;
if((i * 3 + j / 3) == 100){
System.out.println("大和尚数量:" + i + ",小和尚数量:" + j);
}
}
}
}
(6)、百鸡百钱
已知一只公鸡5块钱,母鸡3块钱,小鸡1块钱3只,问100元买100只鸡有哪些方案?
package com.java.method.mianshi;
/**
* 已知一只公鸡5块钱,母鸡3块钱,小鸡1块钱3只,问100元买100只鸡有哪些方案?
* 提示:
* 如果拿100元买公鸡,最多买20个。
* 如果拿100元买母鸡,最多买33个。
*/
public class Test03 {
public static void main(String[] args) {
// 公鸡数量最多20个
for (int i = 0; i <= 20; i++) { // i是公鸡的数量
// 母鸡数量最多是33个
for (int j = 0; j <= 33; j++) { // j是母鸡的数量
// 小鸡的数量
int k = 100 - i - j;
if(k % 3 == 0){
if((i * 5 + j * 3 + k / 3) == 100){
System.out.println("公鸡:"+i+",母鸡:"+j+",小鸡:" + k);
}
}
}
}
}
}
/*
公鸡:0,母鸡:25,小鸡:75
公鸡:4,母鸡:18,小鸡:78
公鸡:8,母鸡:11,小鸡:81
公鸡:12,母鸡:4,小鸡:84
*/
4.1.7、类的主方法
4.1.7.1、main方法概述
java虚拟机通过main方法找到需要启动的运行程序并且检查main函数所在类是否被java虚拟机装载
如果没有装载那么就装载该类并且装载所有相关的其他类
因此程序在运行的时候第一个执行的方法就是main()方法
即main方法的调用者是java虚拟机
当一个类中有main()方法,执行命令"java 类名"则会启动虚拟机执行该类中的main方法
4.1.7.2、main方法格式
主方法的语法如下
public static void main(String[] args) {
// 方法体
}
必须这么定义(这是Java的规范)
在主方法的定义中可以看到主方法具有以下特性
-
访问权限
因为调用者是java虚拟机,java虚拟机不在该类中也不在该类所在的包中更不是该类的子类,所以main函数的权限只能设置为最大的public,便于java虚拟机的调用
-
static
java虚拟机在执行main方法时不必创建对象,所以方法必须是static,也就是在类加载进内存时该方法就可以使用
-
返回数据类型
对于java中的main方法jvm限制不能有返回值,因此返回值类型为void
-
方法名
因为是JVM调用的,因此方法名被定义成main以便JVM调用
-
方法参数类型
main方法中有一个输入参数,类型为String[],用于接收String类型的数组参数
该数据中保存执行java命令时传递给所运行的类的参数
其中args[0]~args[n]分别代表程序的第一个参数到第n+1个参数,可以使用args.length获取参数的个数
这是java的规范,main()方法中必须有一个入参,类细必须String[]
-
参数名
方法参数的名字是自己定义的,但是根据习惯这个字符串数组的名字一般和sun java规范范例中mian参数名保持一致(也就是取名为args)
4.1.7.3、main方法实例
虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们
【示例】
package com.java.method.main;
public class HelloApp {
public static void main(String[] args) {
System.out.println("Hello World!");
for (int i = 0; i < args.length; i++) {
System.out.println(args);
}
}
}
编译后在命令行模式下键入"java HelloApp.java run virtual machine"

将通过调用HelloApp的方法main来启动java虚拟机,传递给main一个包含三个字符串"run"、“virtual”、"machine"的数组
现在略述虚拟机在执行HelloApp时可能采取的步骤
开始试图执行类HelloApp的main方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代表,于是虚拟机使用ClassLoader试图寻找这样的二进制代表
如果这个进程失败则抛出一个异常
类被装载后同时在main方法被调用之前,必须对类HelloApp与其它类型进行链接然后初始化
链接包含三个阶段
-
检验
检查被装载的主类的符号和语义
-
准备
创建类或接口的静态域以及把这些域初始化为标准的默认值
-
解析
负责检查主类对其它类或接口的符号引用,在这一步它是可选的
整个过程如下

2、类的定义
4.2.1、类的定义
类是客观世界中某类群体的一些基本特征的抽象
反过来说具有相同属性和行为的一类实体被称为类
在java中必须先设计类才能获得对象
-
类
是对象共同特征的描述
-
对象
是真实存在的具体实例
4.2.2、类的组成
类包含属性和行为两部分
-
成员变量(属性)
将变量定义在类中方法外即为成员变量
-
成员方法(行为)
将普通方法去掉static关键字即为成员方法
类中对象的属性是以成员变量的形式定义的,对象的行为是以方法的形式定义的
类 = 属性 + 方法
例如:把雁群比作大雁类,那么大雁类就具备了喙、翅膀和爪等属性,觅食、飞行和睡觉等行为,而一只要从北方飞往南方的大雁则被视为大雁类的一个对象
大雁类和大雁对象的关系如图所示

4.2.3、类的定义形式
语法规则如下
[访问控制修饰符] class 类名称{
/**
* 声明成员属性(变量)
*/
数据类型 属性名1;
数据类型 属性名2;
......
数据类型 属性名n;
/**
* 定义方法的内容
*/
访问控制修饰符 返回值的数据类型 方法名称(参数1,参数2...参数n){
//程序语句
[return 表达式;]
}
}
【示例】
package com.java.classDefine;
/*
1. 定义类的语法格式:
[修饰符列表] class 类名{
类体 = 属性 + 方法;
// 属性(实例变量),描述的是状态
// 方法,描述的是行为动作
}
2. 为什么要定义类
因为要通过类实例化对象。有了对象,让对象和对象之间协作起来形成系统。
*/
public class Student00 {
// 属性:姓名,年龄,性别,他们都是实例变量
// 姓名
String name;
// 年龄
int age;
// 性别
boolean gender;
}
(1)、public类名和原文件名一致
如果一个类是public的则类名必须和源文件名保持一致
【示例】
package com.java.classDefine;
public class Animal {
}

(2)、public类最多有一个
public类在一个源文件中如果有的话只能有一个
package com.java.classDefine;
public class Student02 {
}
public class Student03 {
}

(3)、无public类
public的类可以没有
package com.java.classDefine;
class Student03 {
}

(4)、定义多个java类
一个Java文件中可以定义多个class类
编译之后一个class就会对应生成一个class字节码文件
实际开发中建议一个文件定义一个class类
package com.java.classDefine;
public class Student04 {
}
class Student05 {
}
class Student06 {
}

4.2.4、标准的JavaBean
标准javaBean可以理解成实体类,其对象可以在程序中封装数据
- 所有的成员变量都需要使用private关键字私有化
- 为每一个成员变量编写一对get、set方法
- 编写一个无参数的构造方法(必须)
- 编写一个全参数的构造方法(可以没有)
【示例】
package com.java.classDefine;
/*
标准javaBean可以理解成实体类,其对象可以在程序中封装数据
所有的成员变量都需要使用private关键字私有化
为每一个成员变量编写一对get、set方法
编写一个无参数的构造方法(必须)
编写一个全参数的构造方法(可以没有)
*/
public class JavaBeanTest01 {
//定义成员变量
private String name;
private int age;
//定义无参数构造方法
public JavaBeanTest01() {
}
//定义全参数构造方法
public JavaBeanTest01(String name, int age) {
this.name = name;
this.age = age;
}
//定义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;
}
}
4.2.5、构造方法
(1)、构造方法的作用
构造方法的主要作用是创建类的对象和对象的初始化
作用1:对象的创建(通过调用构造方法可以完成对象的创建)
作用2:对象的初始化(给对象的所有属性赋值就是对象的初始化)
(2)、构造方法的定义
[访问控制修饰符] class 类名称{
[访问权限] 类名称(类型1 参数1,类型2 参数2…….){
}
}
构造方法的名称必须与类名一致
-
返回值
构造方法的声明处不能有任何返回值类型的声明,void都不可以写
如果提供了返回值类型那么这个方法就不是构造方法了,就变成普通方法了
-
构造方法可以重载
在java中一个类中可以定义多个构造方法而且这些构造方法自动构成了方法的重载(overload)
-
不能在构造方法中使用return返回一个值
-
java类中一定有构造方法
任何类定义出来,如果没有明确的声明一个构造方法,则在编译时会直接生成一个无参的、什么都不做的构造方法
一旦定义了有参构造器,默认的无参构造器就没有了。当试图调用无参构造方法实例化一个对象时,编译器会报错
所以只有在类中没有定义任何构造方法时,编译器才会在该类中自动创建一个不带参数的构造方法
如果自定义了带参数的构造方法,同时又想有无参数的构造方法,那只能手动定义这个无参数的构造方法
(3)、构造方法的原理
构造方法执行包括两个重要的阶段
-
第一阶段:对象的创建
new的时候会直接在堆内存中开辟空间,然后给所有属性赋默认值,完成对象的创建
这个过程是在构造方法体执行之前就完成了
-
第二阶段:对象的初始化
构造方法体开始执行标志着开始进行对象初始化
构造方法体执行完毕表示对象初始化完毕
这两个阶段不能颠倒也不可分割
【示例】
package com.java.constructor;
/*
构造方法的执行分为两个阶段
1) 对象的创建
当我们使用关键字new时就会在内存中创建一个新的对象
在该阶段完成的工作是创建对象并为对象的属性赋默认值,但还没有被初始化
2) 对象的初始化
执行构造方法的方法体,为对象的属性完成初始化
这两个阶段不能颠倒也不可分割
*/
public class GouZaoTest {
private String name;
private int age;
public GouZaoTest() {
}
public GouZaoTest(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
GouZaoTest test = new GouZaoTest("张三", 20);
}
}

(4)、构造方法的调用
使用new运算符来调用
语法:new 构造方法名(实参);
注意:构造方法最终执行结束之后,会自动将创建的对象的内存地址返回。但构造方法体中不需要提供"return 值;"这样的语句
(5)、构造方法的分类
-
无参数构造器(默认存下的)
初始化对象的时候将成员变量的数据均设置为默认值
-
如果一个类没有显示的定义任何构造方法,系统会默认提供一个无参数构造方法,也被称为缺省构造器。
-
一旦显示的定义了构造方法,则缺省构造器将不存在。为了方便对象的创建,建议将缺省构造器显示的定义出来。
-
-
有参数构造器
在初始化对象的时候同时可以为对象进行赋值
(6)、构造方法VS Setter方法
构造方法中给属性赋值了,为什么还需要单独定义set方法给属性赋值呢?
- 在构造方法中赋值是对象第一次创建时属性赋的值
- set方法可以在后期的时候调用来完成属性值的修改
(7)、构造代码块
位置:类中方法外定义
格式:{}
特点
- 构造代码块与构造方法一样都是在类被实例化的过程中被调用的
- 构造代码块在构造方法之前先执行
- 每一次在new对象的时候都会先执行一次构造代码块
对象的创建原理
- new的时候在堆内存中开辟空间,给所有属性赋默认值
- 执行构造代码块进行初始化
- 执行构造方法体进行初始化
- 构造方法执行结束,对象初始化完毕
使用场景:初始化实例资源(如果所有的构造方法在最开始的时候有相同的一部分代码,不妨将这个公共的代码提取到构造代码块当中,这样代码可以得到复用)
(8)、构造方法的使用
【示例】
package com.java.constructor;
/*
* 构造方法/Constructor/构造器
*
* 1. 构造方法有什么作用
* 作用1:对象的创建(通过调用构造方法可以完成对象的创建)
* 作用2:对象的初始化(给对象的所有属性赋值就是对象的初始化)
*
* 2. 怎么定义构造方法
* [修饰符列表] 构造方法名(形参列表){
* 构造方法体;
* }
* 注意:
* 构造方法名必须和类名一致。
* 构造方法不需要提供返回值类型。
* 如果提供了返回值类型,那么这个方法就不是构造方法了,就变成普通方法了。
*
* 3. 构造方法怎么调用
* 使用new运算符来调用。
* 语法:new 构造方法名(实参);
* 注意:构造方法最终执行结束之后,会自动将创建的对象的内存地址返回。但构造方法体中不需要提供"return 值;"这样的语句。
*
* 4.在java语言中,如果一个类没有显示的去定义构造方法,系统会默认提供一个无参数的构造方法。(通常把这个构造方法叫做缺省构造器。)
*
* 5.一个类中如果显示的定义了构造方法,系统则不再提供缺省构造器。所以为了对象创建更加方便,建议把无参数构造方法手动的写出来。
*
* 6.在java中一个类中可以定义多个构造方法,而且这些构造方法自动构成了方法的重载(overload)。
*
* 7. 构造方法中给属性赋值了,为什么还需要单独定义set方法给属性赋值呢?
* 在构造方法中赋值是对象第一次创建时属性赋的值。set方法可以在后期的时候调用,来完成属性值的修改。
*
* 8.构造方法执行原理?
* - 构造方法执行包括两个重要的阶段:
* 第一阶段:对象的创建
* 第二阶段:对象的初始化
* - 对象在什么时候创建的?
* new的时候,会直接在堆内存中开辟空间。然后给所有属性赋默认值,完成对象的创建。(这个过程是在构造方法体执行之前就完成了。)
* - 对象初始化在什么时候完成的?
* 构造方法体开始执行,标志着开始进行对象初始化。构造方法体执行完毕,表示对象初始化完毕。
*
* 9. 构造代码块?
* 语法格式:
* {}
* 构造代码块什么时候执行,执行几次?
* 每一次在new的时候,都会先执行一次构造代码块。
* 构造代码块是在构造方法执行之前执行的。
*
* 10. 构造代码块有什么用?
* 如果所有的构造方法在最开始的时候有相同的一部分代码,不妨将这个公共的代码提取到构造代码块当中,这样代码可以得到复用。
*/
public class Student01 {
// 构造代码块
{
//System.out.println("构造代码块执行!");
// 这里能够使用this,这说明,构造代码块执行之前对象已经创建好了,并且系统也完成了默认赋值。
//System.out.println(this.name);
for (int i = 0; i < 10; i++) {
System.out.println("iiiiiiiiiii = " + i);
}
}
/**
* 无参数的构造方法显示的定义出来。
*/
public Student01() {
/*for(int i = 0; i < 10; i++){
System.out.println("i = " + i);
}*/
System.out.println("Student类的无参数构造方法执行了");
}
public Student01(String name) {
/*for(int i = 0; i < 10; i++){
System.out.println("i = " + i);
}*/
this.name = name;
}
public Student01(String name, int age) {
/*for(int i = 0; i < 10; i++){
System.out.println("i = " + i);
}*/
this.name = name;
this.age = age;
}
public Student01(String name, int age, boolean sex, String address) {
/*for(int i = 0; i < 10; i++){
System.out.println("i = " + i);
}*/
this.name = name;
this.age = age;
this.sex = sex;
this.address = address;
}
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
/**
* 性别
*/
private boolean sex;
/**
* 家庭住址
*/
private String address;
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 boolean isSex() {
return sex;
}
public void setSex(boolean sex) {
this.sex = sex;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
class Student01Test {
public static void main(String[] args) {
// 调用Student类的构造方法来完成Student类型对象的创建。
// 以下代码本质上是:通过new运算符调用无参数的构造方法来完成对象的实例化。
// s1是一个引用。保存了内存地址指向了堆内存当中的Student类型的对象。
// 这样就完成了学生对象的创建以及初始化。
// 无参数构造方法没有给属性手动赋值,但是系统会赋默认值
Student01 s1 = new Student01();
System.out.println("姓名:" + s1.getName());
System.out.println("年龄:" + s1.getAge());
System.out.println("性别:" + (s1.isSex() ? "男" : "女"));
System.out.println("住址:" + s1.getAddress());
// 通过调用另一个有参数的构造方法来创建对象,完成对象的初始化。
Student01 zhangsan = new Student01("张三", 20, true, "北京朝阳");
System.out.println("姓名:" + zhangsan.getName());
// 修改名字
zhangsan.setName("张三2");
System.out.println("姓名:" + zhangsan.getName());
System.out.println("年龄:" + zhangsan.getAge());
System.out.println("性别:" + (zhangsan.isSex() ? "男" : "女"));
System.out.println("住址:" + zhangsan.getAddress());
Student01 wangwu = new Student01("王五");
System.out.println("姓名:" + wangwu.getName());
System.out.println("年龄:" + wangwu.getAge());
System.out.println("性别:" + (wangwu.isSex() ? "男" : "女"));
System.out.println("住址:" + wangwu.getAddress());
Student01 zhaoliu = new Student01("赵六", 30);
System.out.println("姓名:" + zhaoliu.getName());
System.out.println("年龄:" + zhaoliu.getAge());
System.out.println("性别:" + (zhaoliu.isSex() ? "男" : "女"));
System.out.println("住址:" + zhaoliu.getAddress());
}
}
4.2.6、类的五大成员
(1)、五大成员简介
-
成员变量
- 实例变量
- 静态变量
-
成员方法
- 实例方法
- 静态方法
-
构造方法
-
代码块
- 构造代码块
- 静态代码块
-
内部类
- 成员内部类
- 静态内部类
- 局部内部类
- 匿名内部类
(2)、初始化顺序
实例化对象时对象的所在类的所有成员变量首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建对象
总结
- 先父后子
- 变量优于块
- 静态优于非静态
类初始化的顺序如下
- 父类静态变量
- 父类静态代码块
- 子类静态变量
- 子类静态代码块
- 父类普通变量
- 父类普通代码块
- 父类构造函数
- 子类普通变量
- 子类普通代码块
- 子类构造函数
(3)、测试案例
【示例】
package com.java.classDefine;
public class InitOrderDemo1 {
//构造代码块中如果使用了成员变量的话,那么构造代码块要在成员变量定义之后
/*{
System.out.println(b1);
System.out.println("父类构造代码块1");
}*/
String b1 = "父类实例变量1";
//静态代码块中如果使用了静态变量的话,那么静态代码块要在静态变量定义之后
/*static {
System.out.println(a1);
System.out.println("父类静态代码块");
}*/
static {
System.out.println("父类静态代码块2");
}
static String a1 = "父类静态变量1";
{
System.out.println(b1);
System.out.println("父类构造代码块1");
}
static {
System.out.println(a1);
System.out.println("父类静态代码块1");
}
String b2 = "父类实例变量2";
static String a2 = "父类静态变量2";
static {
System.out.println(a2);
System.out.println("父类静态代码块2");
}
public InitOrderDemo1() {
System.out.println("父类构造方法");
}
public static void superMethod() {
System.out.println("父类普通静态方法");
}
}
class Derived extends InitOrderDemo1 {
public Derived() {
System.out.println("子类构造方法");
}
String b = "子类实例变量";
{
System.out.println(b);
System.out.println("子类实例代码块");
}
static String a = "子类静态变量";
static {
System.out.println(a);
System.out.println("子类静态代码块");
}
public static void derivedMethod() {
System.out.println("子类普通静态方法");
}
public static void main(String[] args) {
//InitOrderDemo1.superMethod();
//Derived.derivedMethod();
new Derived();
}
}
分别执行主函数里的三条指令,执行结果如下
-
InitOrderDemo.superMethod();
父类静态代码块2
父类静态变量1
父类静态代码块1
父类静态变量2
父类静态代码块2
子类静态变量
子类静态代码块
父类普通静态方法

-
Derived.subMethod();
父类静态代码块2
父类静态变量1
父类静态代码块1
父类静态变量2
父类静态代码块2
子类静态变量
子类静态代码块
子类普通静态方法

-
new Derived();
父类静态代码块2
父类静态变量1
父类静态代码块1
父类静态变量2
父类静态代码块2
子类静态变量
子类静态代码块
父类实例变量1
父类构造代码块1
父类构造方法
子类实例变量
子类实例代码块
子类构造方法

3、对象的概念
4.3.1、对象定义
对象是类的具体化
对象有属性和方法两个特性
-
静态部分
静态部分被称为"属性",也成为"实例变量"
任何对象都具备自身属性,每个对象之间是靠属性的不同来进行区分的
例如:人的性别
-
动态部分
动态部分指的是对象的行为,也称为"实例方法"
每个对象所具备的操作就是类中规定好的方法
例如:人可以行走
4.3.2、对象创建
类名 对象名称 = null;
对象名称 = new 构造方法名(形参列表);
或者
类名 对象名称 = new 构造方法名(形参列表);
-
数据类型
定义对象的数据类型,该数据类型属于引用数据类型
-
对象名称
对象名称属于局部变量,保存的是堆内存中对象的内存地址
对象名称有一个特殊的称呼"引用",引用的本质就是一个变量
引用和对象要区分开:对象在JVM堆当中,引用是保存对象地址的变量
-
new
new是一个运算符,用于创建对象
-
构造方法名(形参列表)
类的构造方法
当执行"new 构造方法名(形参列表);"时会在堆内存中开辟一块对象空间
【示例】
package com.java.object.create;
/*
1. 一个类可以实例化多个java对象。
2. 实例变量属于成员变量,成员变量如果没有手动赋值,系统会赋默认值
数据类型 默认值
----------------------
byte 0
short 0
int 0
long 0L
float 0.0F
double 0.0
boolean false
char \u0000
引用数据类型 null
*/
public class Student01 {
// 属性:姓名,年龄,性别,他们都是实例变量
// 姓名
String name;
// 年龄
int age;
// 性别
boolean gender;
}
class Student01Test {
public static void main(String[] args) {
// 局部变量
int i = 10;
// 通过学生类Student实例化学生对象(通过类创造对象)
// Student s1; 是什么?s1是变量名。Student是一种数据类型名。属于引用数据类型。
// s1也是局部变量。和i一样。
// s1变量中保存的是:堆内存中Student对象的内存地址。
// s1有一个特殊的称呼:引用
// 什么是引用?引用的本质上是一个变量,这个变量中保存了java对象的内存地址。
// 引用和对象要区分开。对象在JVM堆当中。引用是保存对象地址的变量。
Student01 s1 = new Student01();
// 修改对象的属性(修改变量的值,给变量重新赋值)
s1.name = "张三";
s1.age = 20;
s1.gender = true;
System.out.println("姓名:" + s1.name); // 张三
System.out.println("年龄:" + s1.age); // 20
System.out.println("性别:" + (s1.gender ? "男" : "女")); // 男
}
}
4.3.3、对象的使用
-
访问成员变量
-
访问实例变量
对象名称.实例变量;
访问实例变量不是能使"类名.实例变量"来访问
-
访问静态变量
对象名称.静态变量;
虽然可以访问,但是不建议使用这种方式
-
-
访问成员方法
对象名称.方法();
访问成员方法不能使用"类名.成员方法()"来访问
(1)、成员变量
成员变量简介
在Java中属性也称为成员变量
成员变量的定义与普通变量的定义一样
语法如下
修饰符 数据类型 变量名称 [ = 值];
[ = 值]表示可选内容,定义变量时可以为其赋值,也可以不为其赋值,但是最好不赋初始值,因为不可能所有的对象初始值都是一样的
如果不设置初始值,则会有默认值。
数据类型 默认值 ---------------------- byte 0 short 0 int 0 long 0L float 0.0F double 0.0 boolean false char \u0000 引用数据类型 null
【示例】
package com.java.object.create;
/*
实例变量属于成员变量,成员变量如果没有手动赋值,系统会赋默认值
数据类型 默认值
----------------------
byte 0
short 0
int 0
long 0L
float 0.0F
double 0.0
boolean false
char \u0000
引用数据类型 null
*/
public class VarDefaultValueTest01 {
byte varByte;
short varShort;
int varInt;
long varLong;
float varFloat;
double varDouble;
char varChar;
boolean varBoolean;
String varString;
}
class VarDefaultValueTest01Test {
public static void main(String[] args) {
VarDefaultValueTest01 s1 = new VarDefaultValueTest01();
System.out.println("varByte:" + s1.varByte); // 0
System.out.println("varShort:" + s1.varShort); // 0
System.out.println("varInt:" + s1.varInt); //0
System.out.println("varLong:" + s1.varLong); //0
System.out.println("varFloat:" + s1.varFloat); //0.0
System.out.println("varDouble:" + s1.varDouble); //0.0
System.out.println("varChar:" + s1.varChar); // \u0000
System.out.println("varBoolean:" + s1.varBoolean); //false
System.out.println("varString:" + s1.varString); //null
}
}

成员变量VS局部变量

成员内存图
【示例】
package com.java.object.create;
/*
实例变量属于成员变量,成员变量如果没有手动赋值,系统会赋默认值
数据类型 默认值
----------------------
byte 0
short 0
int 0
long 0L
float 0.0F
double 0.0
boolean false
char \u0000
引用数据类型 null
*/
public class VarDefaultValueTest01 {
byte varByte;
short varShort;
int varInt;
long varLong;
float varFloat;
double varDouble;
char varChar;
boolean varBoolean;
String varString;
}
class VarDefaultValueTest01Test {
public static void main(String[] args) {
VarDefaultValueTest01 s1 = new VarDefaultValueTest01();
System.out.println("varByte:" + s1.varByte); // 0
System.out.println("varShort:" + s1.varShort); // 0
System.out.println("varInt:" + s1.varInt); //0
System.out.println("varLong:" + s1.varLong); //0
System.out.println("varFloat:" + s1.varFloat); //0.0
System.out.println("varDouble:" + s1.varDouble); //0.0
System.out.println("varChar:" + s1.varChar); // \u0000
System.out.println("varBoolean:" + s1.varBoolean); //false
System.out.println("varString:" + s1.varString); //null
}
}
初始内存图

执行main方法,开辟main方法栈帧

执行main方法第一行代码

执行main方法第二行代码,也就是调用method()方法

在栈空间中开辟method方法的栈帧

将对象的引用变量入栈

在堆空间中创建Student对象的空间

将Student对象的内存地址保存到栈空间的stu引用上

给stu对象的name属性赋值

Method方法执行完毕,弹出栈帧

继续执行main方法的第三行代码

(2)、成员方法
在Java语言中成员方法对应于类对象的行为
它主要用来定义类可执行的操作,它是包含一系列语句的代码块
方法的定义必须在某个类中,定义方法时如果没有指定权限修饰符,方法的默认访问权限为缺省(即只能在本类及同一个包中的类中进行访问)
成员方法中不能有任何静态属性(成员方法与对象相关、静态属性与类有关)
成员方法的定义参考"6.1.2"
【示例】
package com.java.object.use;
// 会员类
public class Vip {
// 类 = 属性 + 方法
// 属性:描述的是状态。
// 方法:描述的是行为。
// 姓名
String name; // 实例变量(对象变量)
// 年龄
int age; // 实例变量(对象变量)
// 会员行为动作
// 购物行为
// 没有添加static的方法被叫做实例方法(对象方法)
public void shopping() {
System.out.println("正在购物!");
}
}
class VipTest01 {
public static void main(String[] args) {
// 创建一个Vip对象
Vip vip1 = new Vip();
// 给name和age属性赋值
vip1.name = "jack";
vip1.age = 20;
System.out.println("name = " + vip1.name);
System.out.println("age = " + vip1.age);
// 为什么name和age不能使用"类名."访问
// 实例变量要想访问必须先new对象,通过引用来访问实例变量
// 实例变量是不能通过类名直接访问的
//System.out.println(Vip.name);
//System.out.println(Vip.age);
// 去购物
// 实例方法需要使用"引用."来调用
vip1.shopping();
// 编译报错:实例方法不能使用"类名."去调用
//Vip.shopping();
}
}
4.3.4、对象序列化
(1)、序列化概念
对象序列化就是把一个对象变为二进制的数据流的一种方法
通过对象序列化可以方便的实现对象的传输或者存储
如果一个类的对象想被序列化则对象所在的类必须实现java.io.Serializable接口
此接口的定义如下
public interface Serializable{}
此接口没有定义任何的方法,所以此接口是一个标识接口,表示一个类具备了被序列化的能力
【示例】
package com.java.object.serializable;
import java.io.Serializable;
public class People1 implements Serializable {
int id;
String name;
People1(int id, String name) {
this.id = id;
this.name = name;
}
public String toString() {
return "id:" + id + " name:" + name;
}
}
以上的Person类已经实现了序列化接口,表示此类的对象是可以经过二进制数据流进行传输的
而如果要完成对象的输入或者输出,还必须依靠对象输出流(ObjectOutputStream)和对象输入流(ObjectInputStream)
- 使用对象输出流输出序列化对象的步骤称为序列化
- 使用对象输入流读取对象的过程成为反序列化
(2)、对象输出流
常用的方法如下
-
ObjectOutputStream(OutputStream out)
传入输出的对象
-
writeObject(Object obj)
输出对象
【示例】
package com.java.object.serializable;
import java.io.*;
public class ObjectOutputStreamTest {
public static void main(String args[]) throws Exception {
File file = new File("E:\\" + File.separator + "test.txt");
OutputStream out = new FileOutputStream(file);
ObjectOutputStream oos = null;
oos = new ObjectOutputStream(out);
oos.writeObject(new People2(20, "zhangsan"));
oos.close();
}
}
class People2 implements Serializable {
int id;
String name;
People2(int id, String name) {
this.id = id;
this.name = name;
}
public String toString() {
return "id:" + id + " name:" + name;
}
}
(3)、对象输入流
使用对象输入流可以直接把被序列化的对象反序列化
常用的方法如下
-
ObjectInputStream(InputStream in)
构造输入对象
-
readObject()
从指定位置读取对象
【示例】
package com.java.object.serializable;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
public class ObjectInputStreamTest {
public static void main(String args[]) throws Exception {
File file = new File("E:" + File.separator + "test.txt");
InputStream input = new FileInputStream(file);
ObjectInputStream ois = null;
ois = new ObjectInputStream(input);
Object object = ois.readObject();
ois.close();
System.out.println(ois);
}
}
4.3.5、匿名对象
(1)、匿名对象
没有名字的对象
(2)、匿名对象特点
- 匿名对象也是一个对象,具有对象的所有功能
- 每一次使用匿名对象时都是一个新的对象,每次创建匿名对象都是不同的对象,一个匿名对象只能使用一次(即匿名对象只能调用一次)
- 匿名对象可以调用属性,但是没有意义,因为调用后就变成垃圾。如果需要赋值还是用有名字对象
- 匿名对象只适合对方法的一次调用,因为调用多次就会产生多个对象,不如用有名字的对象
- 匿名对象只在堆内存开辟空间,不存在栈内存的引用
- 匿名对象可以作为实际参数进行传递
- 只创建对象,但是不用变量来接收
(3)、实例分析
package com.java.object.noname;
public class Point {
private double x, y, z;
public Point(double _x, double _y, double _z) {
x = _x;
y = _y;
z = _z;
}
public void setX(double _x) {
x = _x;
}
public double getPointDistance(Point p) {
return (p.x - x) * (p.x - x) + (p.y - y) * (p.y - y) + (p.z - z) * (p.z - z);
}
public static void main(String[] args) {
Point p = new Point(1.0, 2.0, 3.0);
Point p1 = new Point(0.0, 0.0, 0.0);
System.out.println(p.getPointDistance(p1));
p.setX(3.0);
System.out.println(p.getPointDistance(new Point(1.0, 1.0, 1.0)));
}
}

内存分析图

4.3.6、对象的内存图
对象的内存图分为两种
- 一个对象的内存图
- 多个对象的内存图
(1)、单个对象内存图
示例代码
【示例】
package com.java.object.oneObject;
public class Student {
String name;
int age;
public void study(){
System.out.println("学习Java");
}
public void eat(){
System.out.println("吃饭");
}
}
package com.java.object.oneObject;
public class StudentTest {
public static void main(String[] args) {
Student stu = new Student();
System.out.println(stu);
System.out.println(stu.name);
System.out.println(stu.age);
stu.name = "张三";
stu.age = 23;
System.out.println(stu.name);
System.out.println(stu.age);
stu.study();
stu.eat();
}
}
内存图
初始内存图

加载TestStudent.class字节码文件

执行main方法,在栈中分配main方法的栈帧

加载Student.class字节码

在main方法的栈帧中压如局部变量表

在堆内存中分配对象空间

给对象的属性赋默认值

关联对象与成员方法

将对象的内存地址赋值给栈帧中的对象引用

执行main方法的第二行代码

输出对象的内存地址

继续执行main方法的三、四行代码输出

输出对象的默认属性值

继续执行修改属性值得代码

对象中的属性值修改为修改后的值


执行修改后的输出代码

输出修改后的值

在栈中开发study方法的栈帧

执行study方法的输出语句

输出打印信息

study方法栈帧弹出栈空间

执行stu.eat()

开辟eat方法栈帧

执行eat方法中的代码

输出内容

eat方法弹栈

(2)、两个对象内存图
示例代码
package com.java.object.twoObject;
public class Student {
String name;
int age;
public void study(){
System.out.println("学习Java");
}
public void eat(){
System.out.println("吃饭");
}
}
package com.java.object.teoObject;
public class StudentTest {
public static void main(String[] args) {
Student stu1 = new Student();
stu1.name = "张三";
stu1.age = 23;
Student stu2 = new Student();
stu2.name = "李四";
stu2.age = 24;
System.out.println(stu1.name);
System.out.println(stu2.age);
stu1.study();
stu2.eat();
}
}
内存图
初始内存图

加载Test.class字节码

main方法入栈

加载Student.class字节码

将局部变量stu1压如main方法栈帧的局部变量表

在堆内存中为对象分配空间

给对象的属性赋默认值

将对象的成员方法引用地址关联字节码中的成员方法

将对象的内存地址赋值给对象的引用


执行main方法中赋值代码

修改后的堆内存中对象

创建第二个Student对象

给第二个对象赋默认值

第二个对象的成员方法引用地址指向字节码中的成员方法

将第二个对象的内存地址给栈中的第二个对象的引用

执行修改对象2的代码

执行输出stu1对象的name的代码

打印输出结果

执行输出stu2对象的age属性的代码

打印输出结果

执行"stu1.study()"代码

开辟study方法栈帧

执行输出代码

输出study方法结果

study方法弹栈

执行stu2.study();代码

study方法入栈

执行输出代码

打印输出结果

study方法弹栈

(3)、两个对象指向同一对象
示例代码
package com.java.object.twoSameReference;
public class Student {
String name;
int age;
public void study(){
System.out.println("学习Java");
}
public void eat(){
System.out.println("吃饭");
}
}
package com.java.object.twoSameReference;
public class StudentTest {
public static void main(String[] args) {
Student stu1 = new Student();
stu1.name = "钢门吹雪";
Student stu2 = new Student();
stu2.name = "西域狂鸭";
System.out.println(stu1.name);
System.out.println(stu2.name);
}
}
内存图
初始内存图

加载Test.class字节码

main方法入栈

加载Student.class字节码

在main方法的栈帧中压入局部变量stu1

在堆内存中开辟对象的空间

给对象的属性赋默认值

对象的成员方法引用地址指向字节码的成员方法

将堆内存地址赋值给栈中的对象引用

修改stu1对象的name属性的值

将第二个对象的局部变量stu2入栈

将对象stu1的引用赋值给stu2

修改stu2的值

修改后的值

更多推荐

所有评论(0)