方法
方法执行时的内存变化
- 方法的本质就是将一段具有完整功能的代码块进行封装, 进而将该代码块可以被重复利用
- 方法只定义,不调用是不会分配内存空间的. (从 Java 8 开始, 方法的字节码文件指令存储在元空间metaspace当中. 元空间使用的是本地内存 静态变量存储在元空间中 )
- 法调用时会给该方法在JVM的栈内存中分配空间,此时发生压栈动作。这个方法的空间被称为栈帧。
- 栈帧中主要包括
- 局部变量表
- 操作数栈等。
- 方法执行结束时,该栈帧弹栈,方法内存空间释放。
方法重载
- 编译阶段的机制, 在编译阶段已经完成了方法的绑定, 在编译阶段就已经确定了要调用哪一个方法了
- 在同一个类中, 同样的方法名, 不同的参数, 构成重载
- 不同参数: 主要是为了让JVM作区分
- 参数类型不同
- 参数数量不同
- 参数顺序不同
- 不同参数: 主要是为了让JVM作区分
方法的递归
-
后续IO流获取文件目录会用递归
-
必须要有方法终止执行的条件
-
在实际开发中, 即使有终止条件, 但由于递归太深, JVM栈内存不够大, 也有可能导致栈溢出, 所以能用循环解决就尽量用循环
-
问题: 在开发过程中, 项目中由于递归出现栈溢出, 你会怎么解决?
- 先尝试调大内存 , 进而判断是否是内存容量过小导致, 同时也可以出判断程序是否有问题
- 若调大内存仍无法解决, 大概率是程序出现bug, 这时候再去看代码层面
-
面试题
-
/** * 假如有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,请问第n个月后的兔子有多少对 * 规律: 当月所生兔子总数 = 上月兔子总数 + 上上月兔子总数(上上月有几只兔子,当月就会生几只兔子) * 1 1 2 3 */ public class RecursionTest01 { //递归实现 public static int sumRabbit2(int month){ if (month == 1 || month == 2) { return 1; } return sumRabbit2(month - 1) + sumRabbit2(month - 2); } //循环实现 public int sumRabbit1(int month) { int[] ints = new int[month]; int sum =0; ints[1] = 1; ints[0] = 1; for (int i = 2; i < ints.length; i++) { ints[i]= ints[i-1] + ints[i-2]; sum = ints[i]; } return sum; } @Test void test01() { // System.out.println(sumRabbit1(6)); System.out.println(sumRabbit2(6)); } } -
//使用递归计算n的阶乘 (面试题) public class RecursionDemo02 { public int facTake(int n) { if (n == 1) { return 1; } return n * facTake(n - 1); } @Test public void test01(){ System.out.println(facTake(5)); } } -
计算1+2+3+4+...n的值 的内存图
面向对象
面向对象思想
- 通过现实现象, 将一部分具有共同特征的事物抽象为一个类. 通过一类事物去实例出类中所属对象. 通过操作对象中的属性与行为去操作数据
对象内存结构
- 对象存放与堆中, 主要包含成员变量 , 存储成员方法放在元空间, 堆中只会存放对象中方法的地址值, 当调用方法时jvm通过地址值找到方法并进行入栈操作
成员变量与局部变量
- 存储地址不同, 局部变量存储在栈中, 成员变量存储在堆中
- 作用域不同: 局部变量只能在对应方法中使用 成员变量可以在对应类中的所有方法中使用
- 生命周期不同: 局部变量随着一个方法的执行结束而结束,也就是一个栈帧出栈后即结束 成员变量由对象的存在而存在
- 初始化值不同: 成员变量由初始化值, 局部变量没有
this关键字
-
那个对象调用this this就代表那个对象的地址值, 程序通过this的地址值找到所需要找到的变量
-
如果没有spring这种思想, 成员变量可以是引用数据类型吗?
- 答: 可以是 如果成员变量是引用数据类型, 其内存结构与二维数组一致, 成员变量存储对象地址值,指向对应对象
-
有了spring依赖注入后, 对开发由什么影响 (代码层面)?
-
在一个类中声明另一个类为成员变量时, book变量默认值为空,是不能直接获取到book对象中的成员变量和成员方法的. 其次, new对象只能在方法中,所以想要使用book中的成员,只能在成员方法中new出Book对象并赋值
-
public class Student { Book book; } public void studentDo() { book = new Book(); //方法中new对象 book.readBook(); } }
-
封装
- 封装是一种思想: 在开发中将对象进行封装处理, 使对象数据私有化, 对于一些不需要暴露的数据和方法进行限制,使调用者无需考虑对象内部实现,只需要关注开放出的接口的功能
- 封装的实现方式: 权限修饰符
- private: 只有在本类中的方法可以调用
- (default): 同一个包中可以调用
- public: 同一模块中可以调用
- protected: 同一模块中的子父类
static关键字
-
当一个属性对象是对象级别的, 这个属性通常定义为成员变量. (成员变量是一个对象一份, 100个对象就应该有100个空间)
-
当一个属性是类级别的(所有对象都有这个属性, 并且这个属性值是一样的), 建议将其定义为静态变量, 在内存空间中只有一份, 节省内存空间. 这种类级别的属性不需要new对象, 直接通过类名访问
-
public static void main(String[] args) { //abs是静态方法 int abs = Math.abs(-2); } -
静态变量存储在哪里? 静态变量在什么时后初始化?
- 静态变量存储在堆中( java 8 以后)
- 静态变量在类加载时初始化(赋初值)
- 类加载: 将.class文件加载到方法区中
-
静态变量是在类加载时创建的
继承与多态
/**
* 继承: 减少代码的重复, 子类方法可以重写. 在我看来就是为多态服务的,方法重写机制就可以将子类抽象后重写方法j
* 1,Java不能多继承,但可以多层继承
* 方法的重写:
* 1,在具有继承关系的类中
* 2,具有同样的方法名,参数
* 3,返回值若是引用数据类型,可以是父类返回值的子类 ,例如在父类中返回值是Objet,子类的返回值可以是String
* 4,权限修饰只能更高,不能更低
* 4,静态变量,构造方法,私有变量不可以被继承
*/
public class OverrideTest {
public static void main(String[] args) {
/**
* 多态:
* 程序的执行分为两个阶段
* 1: 编译阶段--->静态绑定
* 当程序执行前, 会先编译, 当程序进行编译时,会根据当前代码进行初步地校验程序是否可以执行, 比如anilam.eat(); 程序编译时
* 会先到Anulam类中寻找是否存在eat()方法,若存在,则编译通过.
* 2: 运行阶段---->动态绑定
* 当程序运行时,JVM会根据引用变量的地址值去寻找对应的调用者,Anilam anilam = new Dog(); 此代码是将堆中dog对象的地址值
* 引用到anilam变量中, 所以程序运行后,会通过地址值获取到dog对象的eat()方法进行执行
*
* 由于程序由于不同的执行状态,所以因为这种执行状态的不同,造成不同的执行结果,称之为多态.
*
* 静态方法与静态变量不能多态! 静态方法与静态变量可以被继承,但不能被重写. 而且静态是类 * 级别的,是通过类名调用的,不需要通过对象引用寻找具体方法
*
*
*/
// 向上转型
Anilam anilam = new Dog();
anilam.eat();
/**
* 由于继承的特性, 也就是有继承关系的两个类,就可以进行向下转型. 由于编译阶段只会判断强制转换的两个类是否有继承关系,
* 若有,则编译认为语法无异常. 但到运行阶段, jvm拿着anilam变量中引用的dog对象的地址值去调用cat对象中的catJob()方法,是找不到的
*
* 如此测诞生了instanceof关运算符
* 作用: 将变量中引用的的地址值与对象判断,是否有继承关系
*/
// ClassCastException:类型转换异常
if (anilam instanceof Cat){
Cat cat = (Cat) anilam;
cat.catJob();
}else if (anilam instanceof Dog){
// 向下转型
Dog dog = (Dog) anilam;
dog.dogJob();
}
}
}
多态的作用
-
降低程序的耦合度,提高程序的扩展力。
-
尽量使用多态,面向抽象编程,不要面向具体编程。
-
class People { public void feed(Cat cat){ cat.eat(); } } class Cat { public void eat(){ System.out.println("猫吃老鼠"); } } class Dog { public void eat(){ System.out.println("狗吃骨头"); } } class Test{ public static void main(String[] args) { People people = new People(); people.feed(new Cat()); /** *若此时人类想要喂养其他动物时, 则需要在People中添加feed(Dog dog)方法. 由于OCP开闭原则, 功能的修改或关闭不修改原代码 * 所以此时程序设计是落后的,耦合度很高 */ // people.feed(new Dog()); } } -
package com.liny.duotai; class People { /** * people.feed(new Dog()); 此时参数构成了多态, feed()方法传入了一个Dog对象, 引用给Zoon, 这便是多态的作用: 通过抽象子类, 将程序解耦! * @param zoon */ public void feed(Zoon zoon){ zoon.eat(); } } class Cat extends Zoon{ public void eat(){ System.out.println("猫吃老鼠"); } } class Dog extends Zoon{ public void eat(){ System.out.println("狗吃骨头"); } } /** * 面向抽象编程, 不要面向具体编程!!! * * 猫,狗,或其他更多的宠物都属于动物, 我们可以将这种具体个体抽象出更高一类, 使用更高类作为参数进行处理, 若以后再有动物添加(比如鸟儿), * 只需要将鸟儿继承动物类即可 */ class Zoon{ public void eat(){}; } class Test{ public static void main(String[] args) { People people = new People(); people.feed(new Cat()); /** *若此时人类想要喂养其他动物时, 则需要在People中添加feed(Dog dog)方法. 由于OCP开闭原则, 功能的修改或关闭不修改原代码 * 所以此时程序设计是落后的,耦合度很高 */ people.feed(new Dog()); } }
软件开发七大原则
- 概念: 软件开发原则旨在引导软件行业的从业者在代码设计和开发过程中,遵循一些基本原则,以达到高质量、易维护、易扩展、安全性强等目标。软件开发原则与具体的编程语言无关的,属于软件设计方面的知识。
- ==开闭原则== (Open-Closed Principle,OCP):一个软件实体应该对扩展开放,对修改关闭。即在不修改原有代码的基础上,通过添加新的代码来扩展功能。(最基本的原则,其它原则都是为这个原则服务的。)
- 单一职责原则:一个类只负责单一的职责,也就是一个类只有一个引起它变化的原因。
- 里氏替换原则:子类对象可以替换其基类对象出现的任何地方,并且保证原有程序的正确性。
- 接口隔离原则:客户端不应该依赖它不需要的接口。
- 依赖倒置原则:高层模块不应该依赖底层模块,它们都应该依赖于抽象接口。换言之,面向接口编程。
- 迪米特法则:一个对象应该对其它对象保持最少的了解。即一个类应该对自己需要耦合或调用的类知道得最少。
- 合成复用原则:尽量使用对象组合和聚合,而不是继承来达到复用的目的。组合和聚合可以在获取外部对象的方法中被调用,是一种运行时关联,而继承则是一种编译时关联。
面向对象中的关键字
- 封装
- 权限修饰符
- static: 表明具有静态属性
- 继承
- extends: 表明一个类型是另一个类型的子类型。对于类,可以是另一个类或者抽象类;对于接口,可以是另一个接口
- abstract: 表明类或者成员方法具有抽象属性
- final: 用来说明最终属性,表明一个类不能派生出子类,或者成员方法不能被覆盖,或者成员域的值不能被改变,用来定义常量
- this: 指向当前实例对象的引用
- super: 用于表示父类结构的标识符
- 接口
- interface: 声明为接口类型
- default: 默认,例如,用在switch语句中,表明一个默认的分支。Java8 中也作用于声明接口函数的默认实现
- implements: 表明一个类实现了给定的接口
- 接口
- 多态
- instanceof: 用来测试一个对象是否是指定类型的实例对象
抽象类与接口
-
抽象类
- 内部结构
- 有构造器, 但无法new对象 (原因: 构造器中存在抽象方法, 抽象方法没有方法体, 若抽象类可以new对象, 当类加载时, 无法知道需要创建具体大小的内存空间)
- abstract关键字不能和private,final,static关键字共存。
- 内部结构
-
接口
- 内部结构
- 无构造器, 无法new对象
- 接口中只能定义:常量+抽象方法。接口中的常量的static final可以省略。接口中的抽象方法的abstract可以省略。接口中所有的方法和变量都是public修饰的。
- Java8之后,接口中允许出现默认方法和静态方法(JDK8新特性)
- 内部结构
-
共同点
- 子类或实现都必须重写或实现父类中所有的抽象方法(==只需要重写抽象方法, 其他没有强制要求==)
-
接口与抽象类如何选择
- 抽象类主要适用于公共代码的提取。当多个类中有共同的属性和方法时,为了达到代码的复用,建议为这几个类提取出来一个父类,在该父类中编写公共的代码。如果有一些方法无法在该类中实现,可以延迟到子类中实现。这样的类就应该使用抽象类。
- 接口主要用于功能的扩展。例如有很多类,一些类需要这个方法,另外一些类不需要这个方法时,可以将该方法定义到接口中。需要这个方法的类就去实现这个接口,不需要这个方法的就可以不实现这个接口。接口主要规定的是行为。
匿名内部类
-
特点
- 特殊的局部内部类,没有名字,只能用一次。
- 匿名内部类只是表征, 其匿名对象的引用就是在方法的参数上-->(Inter inter)
public class InnerDemo {
public static void main(String[] args) {
useInter(new Inter(){
@Override
public void show() {
System.out.println("匿名内部类的输出1");
}
});
//补全写法
useInter((Inter) new Inter() {
@Override
public void show() {
System.out.println("匿名内部类的输出2");
}
});
}
/**
* @param inter 测试接口,不可以new对象
*/
public static void useInter(Inter inter){
inter.show();
}
}
Lambda表达式
-
使用前提条件
- Lambda必须是接口类型。
- 接口中有且仅有一个抽象方法。
-
简化方式
- 任何情况下,参数类型可以省略
- 如果参数列表有且仅有一个参数,()可以省略;
- 如果方法体中有且仅有一条语句,那么{}和;一起省略,如果有return,return也要一起省略。
-
Lambda表达式的好处:
- 简化匿名内部类的书写。
-
Lambda表达式和匿名内部类的区别?
- 能使用Lambda表达式的地方就可以使用匿名内部类, 能使用匿名内部类的地方不一定可以使用Lambda表达式。
- 匿名内部类编译之后有class文件,Lambda表达式编译之后没有class文件。
-
案例:
-
package com.itheima.inner; interface Inter { void show(); } /** * 内部类 */ public class InnerDemo { public static void main(String[] args) { useInter(new Inter() { @Override public void show() { System.out.println("匿名内部类的输出"); } }); //补全写法 useInter((Inter) new Inter() { @Override public void show() { System.out.println("匿名内部类的输出2"); } }); useInter(() -> System.out.println("Lambda表达式的输出")); //直接调用接口方法 ((Inter) ()-> System.out.println("Lambda表达式的输出")).show(); } /** * @param inter 测试接口,不可以new对象 */ public static void useInter(Inter inter) { inter.show(); } }
总结
- 从封装开始, 将现实中的事物封装为对象, 通过操作对象,实现想要的功能, 此时还是面向具体编程. 后续学习静态的概念, 减少了代码的复用, 引入了共享数据的概念. 以此学习继承, 从继承中学习到子类可以继承父类中的属性与方法, 对代码进一步的减少复用. 且有了方法的重写. 为了代码的解耦,开始进入多态的时代(父类的引用指向子类) . 通过重写, 抽象两大概念, 多态成功对代码进行解耦, 将具体事物进行抽象 ,使用多态将父类的引用获取具体实物的对象进行数据操作.