JDK、JRE、JVM
- JDK:Java开发工具箱
- JRE:java运行环境
- JVM:java虚拟机
JDK包括JRE,JRE包括JVM。
JVM是不能独立安装的,JRE和JDK都是可以独立安装的。有单独的JDK安装包,也有单独的JRE安装包,没有单独的JVM安装包。
安装JDK的时候,JRE就自动安装了,同时JRE内部的JVM也就自动安装了。安装JRE的时候,JVM也就自动安装了。
问题1:假设你在软件公司开发了一个新的软件,现在要去客户那边给客户把项目部署一下,把项目跑起来,你需要安装JDK吗?
答:只需要安装JRE就行了。JRE体积很小,安装非常便捷快速。
问题2:为什么安装JDK的时候会自带一个JRE?
答:因为java程序员开发完程序之后,要测试这个程序,让这个程序运行起来,需要JRE。所以JDK安装的时候内部自带一个JRE。
Java的加载与执行(理论比较重要)
java程序非常重要的两个阶段:
- 编译阶段
- 运行阶段
注意:java程序员直接编写的java代码(普通文本)是无法执行被JVM识别的。java程序员编写的java代码这种普通文本必须经过一个编译,将这个“普通文本代码”变成“字节码”,JVM能够识别“字节码”。java代码这种普通文本变成字节码的过程,被称为:编译。
问题1:编译阶段和运行阶段可以在不同的操作系统上完成吗?
答:在windows上编译,编译之后生成了“字节码”,把“字节码”放到linux上运行,完全可以。因为Java是跨平台的,可以做到一次编写到处运行。
数据类型
关于计算机存储单位
计算机只能识别二进制。(1001101100…)
1字节 = 8bit(8比特)即 1byte = 8bit,1bit就是一个1或0。
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
面向对象
概述
“面向对象”(Object Oriented)是一种以对象为中心的编程思想,简称 OO。
特征
面向对象具有三大特征:
- ① 封装(Encapsulation)
- ② 继承(Inheritance)
- ③ 多态(Polymorphism)
类
类的定义
[修饰符] class 类名 {
类体 = 属性 + 方法
}
比如:定义一个学生类
public class Student{
int no;
String name;
int age;
boolean sex;
}
以上程序当中 no、name、age、sex 都是属性,它们都是成员变量中的实例变量,所谓实例变量就是对象级别的变量,这些属性要想访问,必须先创建对象才能访问,不能直接通过类去访问,因为每一个学生的学号都是不一样的。没有学生对象,谈何学号!
对象的创建和使用
知识框架
对象的创建和使用
对象的创建
一个类是可以创建多个对象的,语法格式:new 类名()。
比如:
public class StudentTest {
public static void main(String[] args) {
//创建一个学生对象
Student s1 = new Student();
//再创建一个学生对象
Student s2 = new Student();
//以上代码其实和这行代码差不多
int i = 10;
}
}
对于 Student s1 = new Student() 来说,其中 Student 是一种引用数据类型,s1 是变量名,new Student() 执行之后是一个 Student 类型的对象。 (s1与new Student() 的概念要分清),具体见下图:
对象的使用
public class StudentTest {
public static void main(String[] args) {
//创建一个学生对象
Student s1 = new Student();
//以上代码其实和这行代码差不多
int i = 10;
int no1 = s1.no;
System.out.println("学号:" + no1);
String name1 = s1.name;
System.out.println("姓名:" + name1);
int age1 = s1.age;
System.out.println("年龄:" + age1);
boolean sex1 = s1.sex;
System.out.println("性别:"+ sex1);
}
}
以上程序中并没有给学号赋值,在 java 语言当中,当实例变量没有手动赋值,在创建对象的时候,也就是说在 new 的时候,系统会对实例变量默认赋值,它们的默认值请参考下表:
数据类型 | 默认值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0 |
boolean | false |
char | \u0000 |
引用类型 | null |
深层理解
java虚拟机(JVM)内存管理
① 程序计数器:
- 概念:可以看做当前线程所执行的字节码的行号指示器
- 特点:线程私有的内存
② java 虚拟机栈(重点):
- 概念:描述的是 java 方法执行的内存模型。(每个方法在执行的时候会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用直至完成的过程,就对应一个栈帧从入栈到出栈的过程。)
- 特点 :线程私有,生命周期和线程相同 。这个区域会出现两种异常:StackOverflowError 异常: 若线程请求的深度大于虚拟机所允许的深度。OutOfMemoryError 异常:若虚拟机可以动态扩展,如果扩展是无法申请到足够的内存。
③ 本地方法栈:
- 概念:它与虚拟机栈所发挥的作用是相似的,区别是 java 虚拟机栈为执行 java 方法服务,而本地方法栈是为本地方法服务。
- 特点:线程私有,也会抛出两类异常:StackOverflowError 和OutOfMemoryError。
④ java 堆(重点):
- 概念:是被所有线程共享的一块区域,在虚拟机启动时创建。
- 特点:线程共享,存放的是对象实例(所有的对象实例和数组),GC 管理的主要区域。可以处于物理上不连续的内存空间。
⑤ 方法区(重点):
- 概念:存储已被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码等数据。
- 特点:线程共享的区域,抛出异常 OutOfMemory 异常:当方法区无法满足内存分配需求的时候。
目前,java虚拟机有三块主要的内存空间,分别是“虚拟机栈(后面简栈)”、“方法区”、“堆区”。
- 方法区:存储类的信息
- 栈:存储方法执行时的栈帧以及局部变量
- 堆:主要存储 new 出来的对象,以及对象内部的实例变量
对象和引用的区别:
- 对象是通过 new 出来的,在堆内存中存储
- 引用是:但凡是变量,并且该变量中保存了内存地址指向堆内存当中的对象的。
构造方法Constructor
构造方法是类中特殊的方法,通过调用构造方法来完成对象的创建,以及对象属性的初始化操作。
[修饰符列表] 构造方法名(形式参数列表){
构造方法体;
}
注意:
- ① 构造方法名和类名一致
- ② 构造方法用来创建对象,以及完成属性初始化操作
- ③ 构造方法返回值类型不需要写,写上就报错,包括 void 也不能写
- ④ 构造方法的返回值类型实际上是当前类的类型
- ⑤ 一个类中可以定义多个构造方法,这些构造方法构成方法重载
当一个类没有显示的定义任何构造方法的时候,系统默认提供无参数构造方法,当显示的定义构造方法之后,系统则不再提供无参数构造方法。建议程序员手动的将无参数构造方法写上,因为不写无参数构造方法的时候,这个默认的构造方法很有可能就不存在了。
构造方法虽然在返回值类型方面不写任何类型,但它执行结束之后实际上会返回该对象在堆内存当中的内存地址,这个时候可以定义变量接收对象的内存地址,这个变量就是之前所学的“引用”。
构造方法的作用是专门用来创建对象同时给属性赋值的。
空指针异常
java.lang.NullPointerException 被称为空指针异常。
总之,当一个“空的引用”去访问“对象相关/实例相关”数据的时候,此时一定会发生空指针异常。
封装
知识框架
封装的理解
封装之后就形成了独立实体,独立实体可以在不同的环境中重复使用,显然封装可以降低程序的耦合度,提高程序的扩展性,以及重用性或复用性。另外封装可以隐藏内部实现细节,站在对象外部是看不到内部复杂结构的,对外只提供了简单的安全的操作入口,所以封装之后,实体更安全了。
如何封装
使用java中的修饰符:
private,私有,私有数据只能在本类中访问
public class MobilePhone { //电压:手机正常电压在 3~5V private double voltage; }
对外提供公开的访问入口,让外部程序统一通过这个入口去访问数据。这两个方法通常被称为 set 方法和get 方法。
public class MobilePhone { //电压:手机正常电压在 3~5V private double voltage; public MobilePhone(){} public void setVoltage(double _voltage){ if(_voltage < 3 || _voltage > 5){ //当电压低于 3V或者高于 5V时抛出异常,程序则终止 throw new RuntimeException("电压非法,请爱护手机!"); } //程序如果能执行到此处说明以上并没有发生异常,电压值合法 voltage = _voltage; } public double getVoltage(){ return voltage; } }
this和static
知识框架
static
对于static,看看这篇文章:深入理解static关键字
static 是 java 语言中的关键字,可以用来修饰变量、方法、代码块等
- 翻译为 “ 静态的 ” 。
- 所有static关键字修饰的都是类相关的,类级别的。
- 所有static修饰的,都是采用“ 类名. ”的方式访问,不需要创建对象。
- static修饰的变量:静态变量。
- static修饰的方法:静态方法。
静态变量
变量根据声明的位置进行划分:
- 局部变量:方法体中声明,方法体中访问,方法结束之后局部变量内存就释放了,内存方面局部变量存储在栈当中。
- 成员变量:类体中定义。
成员变量又可以分为:实例变量,静态变量
- 静态变量:使用了 static 关键字,存储在方法区当中,在类加载时初始化
- 实例变量:未使用 static 关键字,属于对象级别,存储在堆内存当中,在构造方法执行过程中初始化
注意:实例相关的,必须先创建对象,通过引用,才能访问,否则可能会出现空指针异常;静态的,不需要对象的参与,直接通过“ 类名 ”即可访问,没有空指针异常的发生。
何时使用?
观看以下Chinese类案例:
PS:只要是方法,不管是静态方法、实例方法,还是构造方法,它们在运行的时候都需要压栈。
以下是未使用静态变量时的内存图:
分析:“中国人类”创建的所有“中国人对象”,“国籍”都是“中国”,不随对象的改变而改变,如果定义为实例变量的话,就会浪费大量堆内存空间。
静态变量在类加载时初始化,不需要new对象,静态变量的空间就开出来了。静态变量存储在方法区:
static String country = “中国”;
以下则是使用了静态变量时的内存图:
分析:建议将“国籍”属性定义为类级别的属性,声明为静态变量,上升为“整个族”的数据,这样的变量不需要创建对象直接使用“类名”即可访问。
注意:静态的(static)也可以使用引用访问,但是建议使用类名访问。引用如果出现空指针,仍然可以访问静态的,不会出现空指针异常。空指针异常只有出现在空引用访问实例相关的,才会出现。
静态代码块
对于静态代码块,详情点击:Static静态代码块以及各代码块之间的执行顺序
静态方法
语法格式:
类{
//静态代码块
static{
java 语句;
}
}
静态代码块在类加载时执行,并且只执行一次。静态代码块实际上是 java 语言为程序员准备的一个特殊的时刻,这个时刻就是类加载时刻。
比如:
public class StaticTest01 {
//静态代码块
static{
System.out.println(2);
}
//静态代码块
static{
System.out.println(1);
}
//main 方法
public static void main(String[] args) {
System.out.println("main execute!");
}
//静态代码块
static{
System.out.println(0);
}
}
运行结果:
2
1
0
main execute!
静态代码块遵循自上而下的顺序依次执行。另外静态代码块当中的代码在 main 方法执行之前执行。
实例:
public class StaticTest02 {
int i = 100;
static{
System.out.println(i);
}
}
运行结果:
错误:无法从静态上下文中引用非静态变量 i
System.out.println(i);
运行结果报错,原因:i 变量是实例变量,实例变量必须先创建对象才能访问,静态代码块在类加载时执行,这个时候对象还没有创建呢,所以 i 变量在这里是不能这样访问的。 可以考虑将实例变量 i 变为静态变量。
this
概述
public class Customer {
private String name;
public Customer(){
}
public Customer(String _name){
name = _name;
}
public void setName(String _name){
name = _name;
}
public String getName(){
return name;
}
}
public class CustomerTest {
public static void main(String[] args) {
Customer c1 = new Customer("张三");
Customer c2 = new Customer("李四");
}
}
this 可以看做一个变量,它是一个引用,存储在 Java 虚拟机堆内存的对象内部,this 这引用保存了当前对象的内存地址指向自身。
this使用在实例方法
this 无法在静态 static 的方法中,static 方法在调用的时候不需要创建对象,直接采用 “ 类名. ” 的方式调用,也就是说,是不需要 “ 当前对象 ” 参与。而 this代表的就是 “ 当前对象 ” 。
this 使用在实例方法当中,代表 “ 当前对象 ” 。
this使用在构造方法
this 使用在构造方法第一行,通过当前构造方法调用本类当中其他的构造方法,其目的是代码复用。
语法:
this(实际参数列表);
实例:
public class Date {
private int year;
private int month;
private int day;
//业务要求,默认创建的日期为 1970 年 1 月 1 日
public Date(){
this(1970 , 1, 1);
}
public Date(int year,int month,int day){
this.year = year;
this.month = month;
this.day = day;
}
//set和get方法略...
}
继承(Inheritance)
继承看一看这篇文章:java继承从“我爸是李刚”讲起
知识框架
概述
继承时子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性,或子类从父类继承方法,使得子类具有与父类相同的行为。所以继承符合的关系:is-a。
如何继承
语法格式:
class 类名 extends 父类名{
类体;
}
实例:银行账户和信用账户(信用账户具有银行账户的性质)
public class Account { //银行账户类
//账号
private String actno;
//余额
private double balance;
//账号和余额的 set 和 get 方法
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
public class CreditAccount extends Account{ //信用账户类
//信誉度(特有属性)
private double credit;
//信誉度的 set 和 get 方法
public double getCredit() {
return credit;
}
public void setCredit(double credit) {
this.credit = credit;
}
}
public class AccountTest {
public static void main(String[] args) {
CreditAccount act = new CreditAccount();
act.setActno("111111111");
act.setBalance(9000.0);
System.out.println(act.getActno() + "信用账户,余额" + act.getBalance() + "元");
}
}
有了继承,才衍生出方法的覆盖和多态。
继承的相关特性
- ① B类继承 A类,则称 A类为超类(superclass)、父类、基类,B类则称为子类(subclass)、派生类、扩展类。
- ② java 中的继承只支持单继承,不支持多继承。
- ③ 虽然 java 中不支持多继承,但有的时候会产生间接继承的效果。
- ④ java 中规定,子类继承父类,除构造方法和被 private 修饰的数据不能继承外,剩下都可以继承。 注意:父类的 private 修饰的属性不能再子类中直接访问,可以间接访问。
测试继承自Object类的方法
以下是Object类的部分源代码:
注意:
尝试调用该方法:
public class ExtendsTest{
public static void main(String[] args) {
ExtendsTest et = new ExtendsTest();
String s = et.toString();
System.out.println(s);
}
}
运行结果:
ExtendsTest@15db9742
等同对象在堆内存的内存地址(经过哈希算法)
执行顺序
难点:类和类继承之后的代码执行顺序
public class Test {
public static void main(String[] args) {
new H2();
}
}
class H1{
//父类代码块
{
System.out.println("父类代码块");//--3
}
public H1(){
System.out.println("父类构造");//--4
}
static{
System.out.println("父类静态代码块");//--1
}
}
class H2 extends H1{
static{
System.out.println("子类静态代码块");//--2
}
//子类代码块
{
System.out.println("子类代码块");//--5
}
public H2(){
System.out.println("子类构造");//--6
}
}
分析:
子类 H2 继承 H1,new H2()执行的时候,会先进行类加载,先加载 H2 的父类 H1,所以 H1 当中的静态代码块先执行,然后再执行 H2 中的静态代码块,静态代码块执行结束之后,不会马上执行构造方法,代码块会先执行。Java 中有一条规则:子类构造方法执行前先执行父类的构造方法。所以父类 H1 的代码块先执行,再执行 H1 的构造方法,然后再执行 H2 的代码块,最后执行 H2 的构造方法。
println()方法
System.out.println(“Hello”);
System.out中,out后面没有小括号,说明out是变量名。另外System是一个类名,直接使用System.out,说明out是一个静态变量。System.out返回一个对象,然后采用“对象.”的方式访问println方法。
类比例子:
public class Test{
//静态变量
static Student stu = new Student();
//程序入口
public static void main(String[] args){
Test.stu.exam();
}
}
class Student{
public void exam(){
System.out.println("考试了!");
}
}
方法覆盖和多态
知识框架
方法覆盖(Override)
以下是一个简单的程序:
public class Test{
public static void main(String[] args){
Person p = new Person();
Chinese c = new Chinese();
American a = new American();
p.eat();
c.eat();
a.eat();
}
}
class Person{
public void eat(){
System.out.println("我要吃饭!");
}
}
class Chinese extends Person{
public void eat(){
System.out.println("我要吃中餐!");
}
}
class American extends Person{
public void eat(){
System.out.println("我要吃西餐!");
}
}
运行结果:
我要吃饭!
我要吃中餐!
我要吃西餐!
以上的程序中,Chinese和American类继承了Person类中的eat方法,并进行了覆盖。之后,子类对象调用了覆盖之后的方法。
构成方法覆盖的条件
- ① 方法覆盖发生在具有继承关系的父子类之间,这是首要条件;
- ② 覆盖之后的方法与原方法具有相同的返回值类型、相同的方法名、相同的形式参数列表,即相同;
注意事项
- ① 由于覆盖之后的方法与原方法一模一样,建议在开发的时候采用复制粘贴的方式,不建议手写;
- ② 私有的方法不能被继承,所以不能被覆盖;
- ③ 构造方法不能被继承,所以也不能被覆盖;
- ④ 覆盖之后的方法不能比原方法拥有更低的访问权限,可以更高
- ⑤ 覆盖之后的方法不能比原方法抛出更多的异常,可以相同或更少
- ⑥ 方法覆盖只是和方法有关,和属性无关;
- ⑦ 静态方法不存在覆盖(不是静态方法不能覆盖,是静态方法覆盖意义不大);
简单示例
public class Test{
public static void main(String[] args){
Cat cat = new Cat();
cat.move();
cat.catchM();
Bird bird = new Bird();
bird.move();
}
}
class Animal{
public void move(){
System.out.println("动物在移动!");
}
}
class Cat{
public void move(){
System.out.println("猫在走猫步!");
}
public void catchM(){
System.out.println("猫在抓老鼠!");
}
}
class Bird{
public void move(){
System.out.println("鸟儿在飞翔!");
}
}
总结
当父类中继承过来的方法无法满足当前子类业务需求的时候,子类有必要将父类中继承过来的方法进行覆盖/重写。
多态
对于多态,向上、向下转型,可以看看这篇文章:深入理解java多态没有烤山药的存在,java就不香了吗?
概述
多态就是“同一个行为”发生在“不同的对象上”会产生不同的效果。
java中允许出现的2中语法:
- 向上转型(Upcasting),子类型转换为父类型,自动类型转换
- 向下转型(Downcasting),父类型转换为子类型,强制类型转换
2中转型的前提是:必须有继承关系,否则报错。
示例及分析
public class Test{
public static void main(String[] args){
Animal a1 = new Cat();
a1.move();
a1.catchM();
Animal a2 = new Bird();
a2.move();
}
}
class Animal{
public void move(){
System.out.println("动物在移动!");
}
}
class Cat{
public void move(){
System.out.println("猫在走猫步!");
}
public void catchM(){
System.out.println("猫在抓老鼠!");
}
}
class Bird{
public void move(){
System.out.println("鸟儿在飞翔!");
}
}
以上程序演示的就是多态。
多态就是“同一个行为(move)”作用在“不同的对象上”会有不同的表现结果。
java 中之所以有多态机制,是因为 java 允许一个父类型的引用指向一个子类型的对象。也就是说允许这种写法:Animal a2 = new Bird()**,属于向上转型(Upcasting),或者叫做自动类型转换**。
对于以下代码的分析:
Animal a1 = new Cat();
a1.move();
java 程序包括编译和运行两个阶段,分析 java 程序一定要先分析编译阶段,然后再分析运行阶段:
- 在编译阶段,编译器只知道 a1 变量的数据类型是 Animal,那么此时编译器会去 Animal.class 字节码中查找 move() 方法,发现 Animal.class 字节码中存在 move()方法,然后将该 move()方法绑定到 a1 引用上,编译通过了,这个过程我们可以理解为“静态绑定”阶段完成了。
- 紧接着程序开始运行。进入运行阶段,在运行的时候实际上在堆内存中 new 的对象是 Cat 类型,也就是说真正在 move 移动的时候,是 Cat 猫对象在移动,所以运行的时候就会自动执行 Cat 类当中的 move()方法,这 个过程可以称为“动态绑定”。但无论是什么时候,必须先“静态绑定”成功之后才能进入“动态绑定”阶段。
Animal a1 = new Cat();
a1.catchM();
上述代码,运行报错。原因:是因为“Animal a = new
Cat();”在编译的时候,编译器只知道 a 变量的数据类型是 Animal,也就是说它只会去Animal.class 字节码中查找 catchMouse()方法,结果没找到,自然“静态绑定”就失败了,编译没有通过。就像以上描述的错误信息一样:在类型为 Animal 的变量 a 中找不到方法catchMouse()。
修改:
//向上转型
Animal a = new Cat();
//向下转型:为了调用子类对象特有的方法
Cat c = (Cat)a;
c.catchMouse();
因此,得出结论:只有在访问子类型中特有数据的时候,需要先进行向下转型。
instance of
示例代码:
public class Test05 {
public static void main(String[] args) {
Animal a = new Bird();
Cat c = (Cat)a;
}
}
以上的代码可以编译,语法上是没有错误的,但是运行时出现错误。这会产生ClassCastException,翻译为类型转换异常,此时需要instance of操作符。
示例代码:
public class Test05 {
public static void main(String[] args) {
Animal a = new Bird();
if(a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse();
} else if(a instanceof Bird){
Bird b = (Bird)a;
b.sing();
}
}
多态的三个必要条件
- ① 继承
- ② 方法覆盖
- ③ 父类型引用指向子类型对象
super关键字
概述
严格来说,super 其实并不是一个引用,它只是一个关键字,super 代表了当前对象中从父类继承过来的那部分特征。
以下有一个例子,便于大家理解 super 和 this。(this 指向一个独立的对象,super 并不是指向某个 “ 独立 ” 的对象)
假设张大明是父亲,张小明是儿子。张小明的眼睛、鼻子和父亲的很像。那么也就是说儿子继承了父亲的眼睛和鼻子特征,那么眼睛和鼻子肯定最终还是长在儿子的身上。假设 this 指向张小明,那么 super 就代表张小明身上的眼睛和鼻子。换句话说 super 其实是 this 的一部分。
如下图所示:张大明和张小明其实是两个独立的对象,两个对象内存方面没有联系,super 只是代表张小明对象身上的眼睛和鼻子,因为这个是从父类中继承过来的,在内存方面使用了 super 关键字进行了标记,对于下图来说 “ this.眼睛 ” 和 “ super.眼睛 ” 都是访问的同一块内存空间。
- super 和 this 都可以使用在实例方法当中
- super 不能使用在静态方法当中,因为 super 代表了当前对象上的父类型特征,静态方法中没有 this,肯定也是不能使用 super 的
- super 也有这种用法:“ super(实际参数列表) ; ”,这种用法是通过当前的构造方法调用父类的构造方法
super使用在构造方法
语法:
super(实际参数列表);
含义:子类构造方法执行过程中调用父类的构造方法。
实例:
public People(String idCard,String name,boolean sex){
this.idCard = idCard;
this.name = name;
this.sex = sex;
}
public Student(String idCard,String name,boolean sex,int sno){
super(idCard,name,sex);
this.sno = sno;
}