本文主要介绍Java中的创建对象的内存分析,参数传递的过程(值传递)以及Java中三大特性之一的多态特性。行文中会穿插Java基础中其他的一些知识点,而这部分知识点也是比较容易疏忽的知识点。
面向对象引入
面向过程和面向对象的区别
-
面向过程:主要的关注点是,实现的具体过程,因果关系
优点:对于业务逻辑比较简单的程序,可以达到快速开发,前期投入成本较低
缺点:1)难以解决非常复杂的业务逻辑;2)元素之间“耦合度”高;3)没有独立体的概念,无法实现组件复用 -
面向对象:主要的关注点是,对象(独立体)能够完成哪些功能
优点:1)容易解决更复杂的业务逻辑;2)耦合度低,扩展力强;3)组件复用性强
缺点:前期投入成本高,需要进行独立体的抽取,大量的系统分析与设计
C语言:纯面向过程,C++:半面向对象,Java:纯面向对象
面向对象的生命周期阶段
- 面向对象的分析:OOA
- 面向对象的设计:OOD
- 面向对象的编程:OOP
对象创建的内存分析
JVM中三块主要的内存空间
- 方法区内存:类加载的时候,classs字节码代码片段被加载到该内存空间
- 栈内存:方法代码片段执行的时候,给该方法的局部变量分配内存空间,在栈内存中压栈
- 堆内存:new出来的对象在堆内存中存储
对象和引用的区别
- 对象:new 运算符在
堆内存
中开辟的内存空间称为对象 - 引用:引用是一个变量,只不过这个变量中保存了另一个Java对象的内存地址
在Java语言当中,程序员不能直接操作堆内存,java中没有指针,不像C语言,程序员只能通过“引用”去访问堆内存当中对象内部的实例变量
访问实例变量的语法格式
- 读取数据:引用.变量名
- 修改数据:引用.变量名 = 值
构造方法的作用
- 创建对象
- 创建对象的同时,初始化实例变量的内存空间
【案例】分析以下代码执行过程中的内存分配情况,特别注意 s变量的内存情况
分析:i变量和s变量在main方法中都是局部变量,局部变量在栈内存中开辟内存空间;用关键字new出来的对象在堆内存中创建内存空间,并且在创建Student对象的时候会创建Student类中的五个实例变量,并初始化(初始化时一切向0看起,no为0,name为null,age为0,sex为false,addr为null);s变量既是局部变量也是引用,它保存堆内存中的对象地址。上述的代码片段的内存图如下所示:
暂且可以这么表示,实际上String类型属于引用数据类型,需要开辟独立的堆内存空间
【案例】分析以下代码片段的内存分配情况。其中User类中的成员变量包括用户身份id标识no,name和addr,Address中的成员变量包括城市名city,街道street和邮编zipcode
public static void main(String[] args) {
User u= new User();
Address a = new Address();
u.addr = a;
System.out.println(u.addr.city);//null
a.city = "天津";
System.out.println(u.addr.city);//天津
}
因为a变量赋值给了u.addr,所以a和u.addr中保存的都是Address对象的内存地址,两者的值相同
JVM相关重要知识点
- 静态变量存储在方法区内存当中
- 三块内存当中变化最频繁的是栈内存,最先有数据的是方法区内存,垃圾回收器主要针对的是堆内存
- 垃圾回收器(自动垃圾回收机制、GC机制)什么时候考虑将某个java对象的内存回收?
堆内存中的java对象成为垃圾数据(没有更多的引用指向这个对象)的时候,会被垃圾回收器回收。这个对象无法被访问,因为访问对象只能通过引用的方式。
空指针异常
public static void main(String[] args) {
Customer c = new Customer();
System.out.println(c.id);
c = null;
System.out.println(c.id);
}
编译通过,运行报错。
上述的代码能够编译通过,因为符合语法;但是运行时出现了空指针异常错误(java.lang.NullPointerException)。空引用访问“实例”相关的数据一定会出现空指针异常。在执行完c = null;后,创建出的Customer对象没有更多的引用指向它,则该对象被GC回收。
Eclipse相关操作
文件夹
- matadata文件夹
在eclipse的工作区当中有一个文件夹:.metadata
该文件夹当中存储了当前eclipse的工作状态(即我们在eclipse中打开了哪些文件,设置了怎么的布局,下次再次打开eclipse的时候仍然进入的是同样的界面)。将 .matadata文件夹删除之后,下一次再次进入这个工作区的时候是一个全新的开始,但是会发现这个IDE中所有的项目丢失了,需要重新导入,磁盘上的项目没有丢。
布局
- Eclipse布局
在Eclipse中我们可以自定义布局,但是一旦我们的布局被我们搞乱了,我们可以恢复布局
【Window】->【Perspective】->【Reset Perspective】
快捷键
- 快捷键
Ctrl+Shift+T :查找某个类文件(Open Type)
Ctrl+Shift+R :查找资源
Ctrl+O :查找类中的属性或方法
Ctrl+1 :代码纠错 - 调试快捷键
F5:Step into(进入方法内部)
F6:Step over (执行下一行)
F7:Step return (返回方法)
F8:Resume(执行到下一个断点)
程序
- 大家所学的类库一般包括三个部分:
源码【可以看源码来理解程序】
字节码【程序开发的过程中使用】
帮助文档【对开发提供帮助】 - 包名的命名规范
公司域名倒序 + 项目名 + 模块名 + 功能名;
采用这种方式重名的几率较低,因为公司域名具有全球唯一性。
扩展
扩展:在一个类中使用了package机制之后,应该如何编译,如何运行?
如A.java类使用了package机制(包名为com.syc.activiti)之后,类名不再是A了,而是com.syc.activiti.A
第一种方式编译运行:
编译:javac 源文件路径
运行:需要先手动方式创建路径(com\syc\activiti),然后将字节码文件剪切到该路径中,然后在com所在的路径运行java com.syc.activiti.A
第二种方式编译运行:
编译:javac -d 编译之后的存放路径 java源文件路径
运行:JVM的类加载器ClassLoader默认从当前路径下加载,需要保证DOS命令窗口的路径先切换到com所在的路径,然后执行java com.syc.activiti.A
面向对象特性
封装
封装步骤
- 所有属性私有化,使用private修饰,数据只能在本类中访问;
- 对外提供简单的操作入口,get方法和set方法,外部程序必须通过这些简单的入口进行访问;
封装的好处
- 对外提供访问方式,隐藏了具体的实现细节。(这样就看不到这个事务的复杂的那一面,只能看到事务简单的那一面)
- 提高了代码的复用性。(封装后的程序可以重复使用,并且适应性较强,在任何场合都可以使用)
- 提高了安全性。(别人不能通过变量名.属性值的方式修改某个私有的成员变量)
Java值传递
Java语言当中方法调用的时候涉及到参数传递的问题,参数传递实际上传递的是变量中保存的具体值,并且一切参数的传递都是值传递。
对比下面的两个程序。
【程序1】
public class MyTest {
public static void main(String[] args) {
int i = 10;
method(i);
System.out.println("main -->" + i); // i=10
}
public static void method(int i) {
i++;
System.out.println("method -->" + i);// i=11
}
}
【程序2】
public class MyTest {
public static void main(String[] args) {
User u = new User(20);
add(u);
System.out.println("main-->" + u.age); //21
}
public static void add(User u) {
u.age++;
System.out.println("add-->" + u.age); //21
}
}
class User{
int age;
public User(int i){
age = i;
}
}
分析:在这两个程序中主要讲的是java中的值传递问题。在下面画出了两个程序的内存图。接下来主要讲解第二个程序。第一步,执行程序入口main方法,创建User对象,开辟堆内存空间,创建成员变量age的空间,并赋初始值20,该对象赋值给User u之后,在栈内存中开辟局部变量u的内存空间,并且其中保存了User对象的内存地址。第二步,调用add方法,将该方法入栈,因为该方法中定义了一个User变量u,因此开辟u的内存空间,因为在调用方法的时候同时把实参u传递给了形参u,相当于将u中保存的内存地址值
给了这个add方法中的u,因此两个u指向的是同一个对象,第三步,u.age++,将u的实例变量age+1,此时,u的age已经是21了,因为add方法中的u和main方法中的u指向的都是同一个对象,因此最终打印出的age值是相同的。
【最终结论】
方法调用的时候,涉及到参数传递的问题,传递的时候,java只遵循一种语法机制,就是将变量中保存的“值”传递过去了,只不过有的时候这个值是一个字面值,有的时候这个值是一个java对象的内存地址。
this
Java语言中的this关键字
- this是一个关键字,译为“这个”
- this是一个引用,this是一个变量,this变量中保存了内存地址指向了自身,this存储在JVM堆内存java对象内部
- 每个对象都有this,创建100个java对象,也就说有100个不同的this
- this可以出现在“实例方法”中,this指向当前正在执行这个动作的对象。(this代表当前对象)
- this在多数情况下都是可以省略不写的
- this不能使用在带有static的方法中
this可以出现的位置
- this 可以使用在实例方法中,代表当前对象
- 可以使用在构造方法中,表示通过当前的构造方法调用其他的构造方法【语法格式:this(实参);】
this(实参); 这种语法只能出现在构造函数的第一行
【错误案例】
public class MyTest {
String name;
public static void main(String[] args) {
}
public static void doSome(){
//以下代码报错
System.out.println(name);//name编译错误
System.out.println(this);//this编译错误
}
}
说明:1)name变量属于成员变量之实例变量,要访问该变量需要先有“当前对象”,static修饰的方法是通过类名的方式访问的,也就是说在这个执行过程中没有“当前对象”;2)this表示的是当前对象,在static修饰的方法中没有当前对象,不能使用this
【结论】
在带有static的方法中不能“直接”访问实例变量和实例方法,static修饰的方法中是没有“当前对象”的,自然无法访问当前对象的实例变量和实例方法。
static
静态方法的调用
- 带有static的方法,其实既可以采用类名的方式访问,也可以采用引用的方式访问
- 但是即使采用引用的方式去访问,实际上执行的时候和引用指向的对象无关!
- 使用eclipse开发工具的时候,使用引用的方式访问带有static的方法,程序会出现警告,但会不报错,所以带有static的方法还是建议使用“类名.”的方式访问
public class Test {
public static void main(String[] args) {
Test.doSome();
doSome();
Test t = new Test();
t.doSome(); //eclipse出现黄色应该,IDEA中 t.的方式点不出来静态方法
t = null;
t.doSome(); //这里不会出现空指针异常
}
public static void doSome(){
System.out.println("do some!");
}
}
上述 t=null; 执行之后仍然能够调用doSome方法,因为静态方法的调用已经和引用指向的对象无关了。
实例变量 OR 静态变量
- 什么时候成员变量声明为实例变量?
所有对象都有这个属性,对象的属性值会随着对象的变化而变化(不同对象的属性值不同) - 什么时候成员变量声明为静态变量?
所有对象都有这个属性,并且所有对象的这个属性的值是一样的,建议定义成静态变量,节省内存开销
静态变量在类加载的时候初始化,内存在方法区中开辟。访问的时候不需要创建对象,直接使用“类名.静态变量名”的方式访问。
可以使用static关键字来定义“静态代码块”
- 语法格式:
static {
Java语句;
} - 静态代码块在类加载时执行,并且只执行一次
- 静态代码块在一个类中可以编写多个,并且遵循自上而下的顺序依次执行
- 静态代码块的作用是什么,什么时候用?
静态代码块是Java为程序员准备的一个特殊的时刻,这个特殊的时刻被称为类加载时刻
。若希望在此刻执行一段特殊的程序,这段程序可以直接放到静态代码块当中,例如要在类加载的时候执行代码完成日志的记录,那么这段日志代码可以写到静态代码块中,完成日志记录。 - 通常在静态代码块当中完成预备工作,先完成数据的准备工作,例如初始化连接池,解析XML配置文件…
实例代码块【了解,使用的非常少】
- 实例代码块在一个类中可以编写多个,也是遵循自上而下的顺序依次执行
- 实例代码块在构造方法执行之前执行,构造方法执行一次,实例代码块对应执行一次
- 实例代码块也是Java语言为程序员准备的一个特殊时刻,这个特殊的时刻被称为
对象初始化时刻
public class Test {
public Test() {
System.out.println("默认构造器");
}
public Test(int a){
System.out.println(a);
}
//实例代码块
{
System.out.println(1);
}
//实例代码块
{
System.out.println(2);
}
public static void main(String[] args) {
Test t = new Test();
Test t2 = new Test(5);
}
}
静态方法
方法什么时候定义为静态的?
大多数方法都定义为实例方法,一般一个行为或者一个动作在发生的时候,都需要对象的参与。但是也有例外的,例如:大多数“工具类”中的方法都是静态方法,编写工具类是为了方便编程,为了方便方法的调用,自然不需要new对象是最好的。
继承
继承的重要基础知识
- 继承“基本”的作用是:
代码复用
。但是继承最“重要”的作用是:有了继承才有了以后“方法的覆盖”
和“多态机制”
。 - 继承中的术语:
B类继承A类,其中:
A类被称为:父类、基类、超类、superclass
B类被称为:子类、派生类、subclass - 子类继承父类的哪些数据?
私有的不支持继承
构造方法不支持继承
其他数据都可以继承 - Java语言只支持单继承,但是一个类也可以间接继承其他类
以下程序输出为:this is B (C的基类为B类)
public class MyTest {
public static void main(String[] args) {
C c = new C();
c.doSome();
}
}
class A{
public void doSome(){
System.out.println("this is A");
}
}
class B{
public void doSome(){
System.out.println("this is B");
}
}
class C extends B{
}
方法覆盖/重写
回顾方法重载
- 方法重载为Overload
- 方法重载什么时候使用?
在同一个类中,方法完成的功能是相似的,建议方法名相同,这样方便程序员的编程,就像在调用一个方法似的,代码美观。 - 什么条件满足之后构成方法重载?
- 在同一个类当中
- 方法名相同
- 参数列表不同:类型、顺序、个数
- 方法重载和什么无关?
- 和方法的返回值类型无关
- 和方法的修饰列表无关
方法覆盖
- 方法覆盖又被称为方法重写,英文为override/overwrite
- 什么时候使用方法重写?
父类中的方法已经无法满足当前子类的业务需求,子类有必要将父类中继承过来的方法进行重新编写,这个重新编写的过程称为方法重写/方法覆盖 - 什么条件满足之后发生重写?
方法重写发生在具有继承关系的父子类之间 - 方法重写的时候尽量复制粘贴,不要编写,容易出错,导致没有产生覆盖
- 注意:私有方法不能继承,所以不能覆盖;构造方法不能继承,所以不能覆盖;静态方法不存在覆盖;覆盖只针对方法,不谈属性。
多态
多态涉及的概念
- 向上转型(upcasting):子类型–>父类型,又被称为自动类型转换
- 向下转型(downcasting):父类型->子类型,又被称为强制类型转换
- 无论是向上转型还是向下转型,两种类型当中必须要有继承关系。没有继承关系,程序是无法编译通过的。
使用多态
public class MyTest {
public static void main(String[] args) {
Animal a = new Cat();
a.move();
}
}
class Animal{
public void move(){
System.out.println("动物在走路");
}
}
class Cat extends Animal{
public void move(){
System.out.println("猫在走路");
}
public void eatFish(){
System.out.println("猫吃鱼");
}
}
【向上转型】
在上面的代码中我们看到Animal a = new Cat(); Java中是允许这种语法的,即父类引用指向子类对象,也即向上转型。但是我们却不能写a.eatFish(); 因为编译器知道a为Animal类型,但是调用a.move();的时候打印的是“猫在走路”。说明如下:
- java程序永远分为编译阶段和运行阶段
- 编译阶段编译器检查a这个引用的数据类型为Animal,由于Animal.class字节码当中有move()方法,所以编译通过了。这个过程称为
静态绑定
,编译阶段绑定,只有静态绑定成功之后才有后续的运行。(对于eatFish方法,因为在字节码文件中没有找到该方法,导致静态绑定失败,没有绑定成功也就是编译失败) - 在运行阶段,JVM堆内存当中真实创建的对象是Cat对象,那么上述程序在运行阶段一定调用Cat对象的move方法,此时发生了程序的
动态绑定
,运行阶段绑定。 - 父类引用指向子类对象这种机制导致程序在编译阶段绑定和运行阶段绑定两种不同的形态/状态,这种机制称为一种多态语法机制。
【向下转型】
需求:要想让上述的程序执行eatFish方法,该怎么办?
a的类型是Animal(父类),转换成Cat(子类),被称为向下转型/downcasting/强制类型转换。当调用的方法是子类型中持有,在父类型中不存在,必须进行向下转型。当然,向下转型也需要两种类型之间必须有继承关系,不然编译报错。强制类型转换需要加强制类型转换符。
什么时候进行向下转型?
答:需要访问子类对象当中特有的方法的时候
Cat c = (Cat) a;
c.eatFish();
强制类型转换错误
若接着上面的程序,我们也一个鸟类(Bird继承了Animal类),并且创建了一个鸟类对象,但是在下转型的时候却转为了Cat型,此时就发生了强制类型转换错误。
Animal b = new Bird();
b.move();
Cat c2 = (Cat) b;
Exception in thread “main” java.lang.ClassCastException: Bird cannot be cast to Cat
at MyTest.main(MyTest.java:23)
分析:(1)编辑阶段,编译器检查b的类型是Animal,Animal和Cat之间存在继承关系,Animal是父类,Cat是子类,可以进行向下转型,语法合格。(2)运行阶段,JVM内存当中真实存在的对象是Bird类型,Bird对象无法转换成Cat对象,因为两种类型之间不存在任何继承关系,此时便出现了著名的强制类型转换错误。
如何避免强制类型转换错误
使用 instanceof 进行类型判断。Java规范中要求:在进行强制类型转换之前,建议采用instanceof运算符进行判断,避免ClassCastException异常的发生。这是一种编程的好习惯。
Animal b = new Bird();
b.move();
if(b instanceof Bird){
Bird b2 = (Bird) b;
b2.move();
b2.fly();
}
else if(b instanceof Cat){
Cat c2 = (Cat) b;
c2.move();
c2.eatFish();
}
多态的作用
- 降低程序的耦合度,提高程序的扩展力
- 核心是:面向抽象编程,尽量不要使用面向具体编程
【以下案例说明多态的作用】
场景是:有一个主人要给宠物喂食,宠物分猫和狗。抽象为主人有喂养动作,宠物有吃食物动作。主人给猫喂食时显示猫吃鱼,给狗喂食时显示狗啃骨头。
未使用多态:
public class MyTest {
public static void main(String[] args) {
Master zhangsan = new Master();
Cat cat = new Cat();
zhangsan.feed(cat);
Dog dog = new Dog();
zhangsan.feed(dog);
}
}
class Master{
public void feed(Cat cat){
cat.eat();
}
public void feed(Dog dog){
dog.eat();
}
}
class Cat{
public void eat(){
System.out.println("猫吃鱼");
}
}
class Dog{
public void eat(){
System.out.println("狗啃骨头");
}
}
分析一波:若主人又添加了一个宠物,如鹦鹉,当给鹦鹉喂食时,需要在Master类中重载feed方法,这样程序扩展极其不便,程序改动大。下面使用多态来实现。
使用多态:
public class MyTest {
public static void main(String[] args) {
Master zhangsan = new Master();
Cat cat= new Cat();
zhangsan.feed(cat);
Dog dog = new Dog();
zhangsan.feed(dog);
}
}
class Master{
public void feed(Pet pet){ //父类引用指向子类对象
pet.eat(); //运行态调用的是实例对象的方法
}
}
class Cat extends Pet{
public void eat(){
System.out.println("猫吃鱼");
}
}
class Dog extends Pet{
public void eat(){
System.out.println("狗啃骨头");
}
}
class Pet{
public void eat(){
}
}
在上述方法中,我们只要定义一个宠物类,然后让其他的具体的宠物都类继承这个类,在喂食动作中,我们放进去的形参也只是宠物,这样在用给具体的宠物喂食时就用到了自动类型转换(静态绑定),而在pet.eat();时却是具体的宠物的吃食动作(动态绑定)。
final
- final是一个关键字,表示最终的,不可变的
- final修饰的类无法被继承
- final修饰的方法无法被覆盖
- final修饰的变量一旦赋值之后,不可重新赋值
- final修饰的实例变量必须手动赋值,不能采用系统默认值
- final修饰的引用一旦指向某个对象之后,不能再指向其他对象,并且被指向的对象无法被垃圾回收器回收(必须等到程序运行结束);final修饰的引用虽然指向某个对象之后不能指向其他对象,但是所指向的对象内部的值是可以被修改的
- final修饰的实例变量是不可变的,这种变量一般和static联合使用,被称为常量。
常量定义的语法格式:public static final 类型 常量名 = 值;
如public static final double PI = 3.1415926;(Java规范中要求所有常量的名字全部大写,每个单词之间使用下划线连接)
对第5点说明:实例变量有默认值+final修饰的变量一旦赋值不能重新赋值。综合考虑,Java语言最终规定实例变量使用final修饰之后必须手动赋初值,不能采用系统默认值。
public class MyTest {
// final int age;//编译错误
//第一种解决方案
final int age = 10;
//第二种解决方案
final int num;
public MyTest(){
num = 10;
}
public static void main(String[] args) {
final int a;
a = 10;
//不可二次赋值
// a = 20;
}
}
在构造方法中为final修饰的实例变量赋值的效果与在定义时赋值的效果是等效的,因为这两者的赋值时间相同,都是在构造方法执行过程中给实例变量赋值。