什么是面向对象(Object Oriented,OO)?
面向对象就是使用对象进行程序设计(是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法)。
对象代表现实世界中可以明确标识的一个实体。例如:一个学生,一张桌子,一个圆,一个按钮甚至一笔贷款都可以看着一个对象。每个对象都有自己独特的标识、状态和行为。
什么是面向对象程序设计((Object Oriented Programming,OOP)?
面向对象程序设计((Object Oriented Programming,OOP)是当今的主流程序设计范型,是一种计算机编程架构,取代了20世纪70年代的“结构化”或过程式编程技术。
OOP的一条基本原则是计算机程序由单个能够起到子程序作用的单元或对象组合而成。
OOP达到了软件工程的三个主要目标:重用性、灵活性和扩展性。OOP=对象(Object)+类(Class)+继承(Inheritance)+多态( Polymorphism)+消息(Communication),其中核心概念是类和对象。
对象
“工欲善其事,必先利其器。”
学习面向对象程序设计(OOP),我们首先要搞清楚关于对象的三个概念:
(1) 行为
(2) 状态
(3) 标识
思考:
(1)什么是对象的行为(behavior)?
——对象的行为即可以对这个对象进行哪些操作,或者可以对这个对象应用哪些方法?
一个对象的行为(behavior,也称为动作(action))是由方法定义的。调用对象的一个方法就是要求对象完成一个动作。例如:可以为圆设置半径,可以计算圆的面积,还可以获取它的周长等。java使用方法定义动作。
(2)什么是对象的状态(state)?
——调用那些方法时,对象会如何响应?
一个对象的状态(state,也称未特征(property)或属性(attribute))是由具有当前值的数据域来表示的。
例如:圆对象具有一个数据域radius,它是标识圆的属性。一个矩形对象具有数据域width和height,它们都是矩形的属性。java使用变量定义数据域。
(3)什么是对象的标识(identity)?
——如何区分可能有相同行为和状态的不同对象?
什么是类(class)?什么是类的实例(instance)?
我们可以这样来理解,现实世界中,学生是一种统称,抽象概念,而
具体的学生(
有诸如
(状态)属性(attribute):学号(sno)、姓名(name)、性别(sex)
和
(行为)方法(method):study()
等属性和方法。)例如:张三、李四、王五等都是一个具体的学生。
即将大体上,统称上的抽象概念“学生”定义为一个类(class),而具体的学生则是类的一个实例(instance)。
即理解了类(class)和实例(instance)的概念,基本上就明白了什么是面向对象编程。
使用一个通用类来定义同一类型的对象。类是一个模板,蓝本或者说是合约,用来定义对象的数据域是什么以及方法是做什么的。
一个对象是类的一个实例。可以从一个类中创建多个实例。创建实例的过程称为实例化。对象(object)和实例(instance)经常是可以互换的。类和对象之间的关系类似于打印机(类)和纸张(对象)之间的关系。
可以用一种指定的打印格式打印出任意多的具有相同内容格式的纸张。
图示:Circle类和它的三个对象(object)。
Circle类UML类图示例:
类(class)是一种对象模板,它定义了如何创建实例,因此,class本身就是一种数据类型,在代码中实际演示如下:
主体类:Circle
/**
* 使用一个通用类来定义同一类型的对象
* 一个对象的状态(state,也称未特征(property)或属性(attribute))是由具有当前值的数据域来表示的,java使用变量定义数据域。
* 一个对象的行为(behavior,也称为动作(action))是由方法定义的,java使用方法定义动作。
*/
public class Circle {
public static final double PI = 3.14; //定义常量PI
/**
* 半径
*/
public double radius;
/**
* 获取圆面积
* @return 圆面积
*/
public double getArea() {
return PI * radius * radius;
}
/**
* 获取圆周长
* @return 圆周长
*/
public double getPerimeter() {
return 2 * PI * radius;
}
/**
* 设置圆半径
* @param newRadius 圆半径
*/
public void setRadius(double newRadius) {
radius = newRadius;
}
}
测试类:CircleTest
public class CircleTest {
public static void main(String[] args) {
//类本身是一种数据类型(它定义了如何创建实例),对象通过类创建===>new
Circle circle1 = new Circle(); //调用对应的构造方法
//给circle1对象设置半径为1
circle1.setRadius(1); //调用对象的一个方法就是要求对象完成一个动作
System.out.println(circle1 + ":" + circle1.getArea()); //返回对象的内存地址及圆面积,直接输出对象,即返回对象的内存地址
System.out.println(circle1 + ":" + circle1.getPerimeter());
Circle circle2 = new Circle();
circle2.radius = 6;
System.out.println(circle2 + ":" + circle2.getArea());
System.out.println(circle2 + ":" + circle2.getPerimeter());
Circle circle3 = new Circle();
circle3.setRadius(108);
System.out.println(circle3 + ":" + circle3.getArea());
System.out.println(circle3 + ":" + circle3.getPerimeter());
}
}
运行结果:
而instance是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同
要想使用对象,必须先构造对象并指定其初始状态。和创建数组一样,通过new操作符从构造方法创建一个对象。new Circle(1)创建一个半径为1的对象,new Circle(6)创建一个半径为6的对象,new Circle(108)创建一个半径为108的对象。
注意,radius变量,会默认初始为0
Circle circle4 = new Circle();
System.out.println(circle4 + ":" + circle4.getArea());
System.out.println(circle4 + ":" + circle4.getPerimeter());
运行结果:
这三个对象(通过circle1,circle2,circle3来引用)有不同的数据,但是有相同的方法。因此可以使用getArea()方法计算它们各自的面积。这三个对象是相互独立的。可以使用点操作符(.)通过对象引用访问数据域及调用方法。
数据域radius称为实例变量,因为它依赖于某个具体的实例。基于同样的原因,getArea()及getPerimeter()方法称为实例方法,因为只能在具体的实例上调用它。调用对象上的实例方法的过程称为调用对象。
使用构造方法构造对象
构造方法在使用new操作符创建对象的时候被调用。
构造方法时一种特殊的方法:
- 构造方法必须具备和所在类相同的名字
- 构造方法没有返回值类型
- 构造方法是在创建一个对象使用new操作符时调用的。构造方法的作用是初始化对象。
构造方法与它所在的类具有完全相同的名字。和其它方法一样,构造方法也可以重载(也就是说,可以有多个同名构造方法,但它们有不同的参数列表),这样更易于用不同的初始数据值来构造对象。一个类可以不定义构造方法。在这种情况下,编译器回自动添加一个方法体为空的公共的无参构造方法。这个构造方法称为默认构造方法。当且仅当类中没有明确定义任何构造方法时才会自动提供它。
带参构造和无参构造方法示例:
public class Student {
String name; //引用类型,默认值为null
int age; //数值型默认值为0
boolean isScienceMajor; //boolean类型默认值为false
char gender; //字符型默认值为'\u0000'
/*
* 注意:按照一般规范,构造方法一般在成员变量后面,行为(方法)前面
* 无参构造方法一般在含参构造方法前面,成员变量后面
*/
public Student() { //无参构造方法,系统默认
}
/*
* 带参构造方法,根据对象引用初始化需求调参
* 无参构造方法的重载
*/
public Student(String name, int age, boolean isScienceMajor, char gender) {
this.name = name;
this.age = age;
this.isScienceMajor = isScienceMajor;
this.gender = gender;
}
}
通过引用变量访问对象
新创建的对象在内存中被分配空间。它们可以通过引用变量来访问。对象的数据和方法可以通过点操作符(.)通过对象的引用变量进行访问。
使用构造方法构建对象示例,声明对象引用变量、创建对象以及将对象的引用赋值给这个变量。
//声明对象引用变量、创建对象以及将对象的引用赋值给这个变量
Circle circle1 = new Circle();
变量circle1存放的是对Circle对象的一个引用。在创建一个对象后,它的数据域和方法可以使用点操作符(.)来调用和访问。该操作符也称为对象成员访问操作符。
例如:circle1.radius引用circle1的半径,而circle1.getArea()调用circle1的getArea方法。方法作为对象上的操作被调用。
数据域radius称作实例变量,因为它依赖于某个具体的实例。基于同样的原因,getArea()方法称为实例方法,因为只能在具体的实例上调用它。调用对象上的实例方法的过程称为调用对象。
引用数据域和null值
数据域也可能是引用类型的。
这里以学生类举例说明:
class Student{
String name; //引用类型,默认值为null
int age; //数值型默认值为0
boolean isScienceMajor; //boolean类型默认值为false
char gender; //字符型默认值为'\u0000'
}
如果一个引用类型没有引用任何对象,那么这个数据域就有一个特殊的java值null。null同true和false一样都是一个直接量。true和false是boolean类型的直接量,null是引用类型直接量。
引用类型数据域的默认值为null,数值型数据域默认值为0,boolean类型数据域默认值为false,字符型数据域默认值为'\u0000',即一个空格符。java没有给方法中的局部变量赋默认值,必须对其进行初始化才能使用。
这里以Student类示例举例并说明:
构造方法构建对象,实例变量(成员变量)的调用
/**
* 学生类
* @author nickwind
*
*/
public class Student {
/*
* 成员变量
*/
String name; //引用类型,默认值为null
int age; //数值型默认值为0
boolean isScienceMajor; //boolean类型默认值为false
char gender; //字符型默认值为'\u0000'
public static void main(String[] args) {
Student student = new Student(); //构造方法创建对象,初始化对象
System.out.println("name:" + student.name); //输出学生姓名
System.out.println("age:" + student.age); //输出学生年龄
System.out.println("isScienceMajor:" + student.isScienceMajor); //专业是否是科学专业, true: 是 false: 不是
System.out.println("gender:" + student.gender); //输出学生性别
}
}
运行结果:
局部变量未初始化,直接调用(编译不通过,抛出异常)
public class Test{ //测试类
public static void main(String[] args) {
/*
* 在main方法中定义的变量为局部变量
*/
//局部变量未初始化使用
int x; //未初始化,即未给变量x赋值
String y; //未初始化,即未给变量y赋值
System.out.println("x:" + x);
System.out.println("y:" + y);
}
}
运行结果示例:
方法内变量,即局部变量需要初始化赋值才能使用,否则编译不通过,报错。
The local variable x may not have been initialized : 即局部变量x未被初始化,未赋值
The local variable y may not have been initialized : 即局部变量y未被初始化,未赋值
注意:当调用值为null的引用变量上的方法时,会发生一个名为NullPointerException的异常,这是一种常见的运行时异常。在通过引用变量调用一个方法前。确保先将对象引用赋值给这个变量。
public class Student {
/**
* 成员方法
*/
public void study() {
System.out.println("小明来到大学后,决定好好学习!");
}
public static void main(String[] args) {
Student student2 = null; //给引用变量student2赋值为null
student2.study(); //调用引用变量student2的成员方法study()
}
}
运行结果:
对象在内存空间的存储情况
以Student类为例,创造一个测试类StudentTest,在测试类中,调用Student类无参构造方法完成两个对象 zhangsan, lisi 的初始化。
public class StudentTest(){
public static void main(String[] args) {
Student zhangsan = new Student();
System.out.println("zhangsan的姓名:" + zhangsan.name);
System.out.println("zhangsan的年龄:" + zhangsan.age);
System.out.println("zhangsan的专业是否为科学专业:" + zhangsan.isScienceMajor);
System.out.println("zhangsan的性别:" + zhangsan.gender);
System.out.println("---------------------");
Student lisi = new Student();
System.out.println("lisi的姓名:" + lisi.name);
System.out.println("lisi的年龄:" + lisi.age);
System.out.println("lisi的专业是否为科学专业:" + lisi.isScienceMajor);
System.out.println("lisi的性别:" + lisi.gender);
}
}
运行结果:
堆栈图如下:
从上述 zhangsan、lisi 两个对象的堆栈图中,我们可以看出,每个对象都拥有一个独立的数据域,在堆中开辟的内存空间地址也不一样。
基本类型变量和引用类型变量的区别
每个变量都代表一个存储值的内存位置。声明一个变量时,就是告诉编译器这个变量可以存放什么类型的值。对基本类型变量来说,对应内存所存储的值是基本类型。对引用类型变量来说,对应内存所存储的值是一个引用,是对象的存储地址。例如:Date(日期类)对象birthday的值存的是一个引用。它指明这个Date对象存储在内存中的什么位置。
import java.util.Date; //导入日期类工具包
public class Student {
/*
* 定义一个日期类对象:birthday
* birthday的值存储的是一个引用,它指名这个Date对象存储在内存中的什么位置
* 由于这里的对象birthday并未在堆内存中开辟空间,故其引用所指向为null
*/
public Date birthday;
public static void main(String[] args) {
Student testStudent = new Student();
System.out.println(testStudent.birthday);
}
}
运行结果:
由于这里的对象birthday并未在堆内存中开辟空间,故其栈中对象引用所指向堆中的存储空间地址为null。
将一个变量赋值给另一个变量时,另一个变量就被赋予同样的值。对基本类型变量而言,就是将一个变量的实际值赋给另一个变量。对引用变量而言,就是将一个变量的引用赋给另一个变量。如下图所示,赋值语句 i = j 将基本类型变量 j 的值复制给基本类型变量 i ;对引用变量来讲,赋值语句deadline = birthday 是将 birthday 的引用赋给 deadline 。赋值之后,变量 birthday 和 deadline 指向同一个对象。
以上述学生类中对象zhangsan、lisi为例,代码演示:
public Date birthday;
zhangsan.birthday = new Date();
lisi.birthday = zhangsan.birthday; //将lisi的birthday引用指向zhangsan的birthday
堆栈图:
注:所有的Java对象都存储在堆中。当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针。在C++中,指针十分令人头疼,因为它们很容易出错。稍不小心就会创建一个错误的指针,或者内存管理出问题。在Java语言中,这些问题都不复存在。如果使用一个没有初始化的指针,运行时系统将会产生一个运行时错误,而不是生成一个随机的结果。同时,不必担心内存管理问题,垃圾回收器会处理相关的事宜。
基本类型数据传递和引用类型数据传递
//无返回值静态方法
public static void change(int age, Student student) {
age = 140;
System.out.println("change-age:" + age);
student.age = 5;
System.out.println(student + "age = " + student.age);
}
//main方法中:
int age = 20;
change(age, zhangsan);
System.out.println("main-age:"+ age);
System.out.println(zhangsan + "age = " + zhangsan.age);
运行结果:
堆栈图:
根据上述堆栈图分析,main方法中是将变量age的值:20传递给了change()方法中作为形参,而main方法中对象zhangsan是将引用(即对象zhangsan在堆内存开辟空间的地址)传递给了change()方法中形参对象student,即对象student和zhangsan的引用所指向堆内存空间中的同一个地址,而由于change()方法中改变了成员变量age的值,因为两个对象的引用指向同一空间,故最后输出的成员变量age的值是一样的。
main方法入栈后,change()方法入栈,参数传递完毕后,change()方法调用完毕出栈并销毁,此时main方法中的age变量不会受到影响,因为仅为值传递,变量age在内存空间中的地址并未传递给change()方法中的age变量,即这两个不同方法中的age变量的地址不一样。
思考一个小问题:为什么输出java对象时,会自动调用toString方法,打印地址?
要想快速弄明白这个问题,查源码自然是最直接有效的。
在查源码之前,补充一个知识点:rt.jar
什么是jar文件?
jar文件是Java归档文件,它存储Java类文件和程序所需的任何资源。它还可以包含一个清单文件,其中可以包含使其成为可执行JAR的主类条目,可以使用java -jar命令运行该文件。
在了解 rt.jar 文件之前,我们需要知道在jdk9版本时,对Java运行时做了重构,已删除了rt.jar、tools.jar、dt.jar以及其它各种内部JAR包。即想查阅 rt.jar 包,需要在jdk8及以前的版本中进行查询。
rt.jar代表runtime,包含核心运行时环境的所有编译文件。
(jdk8版本及以前)在项目类路径中必须包含rt.jar,否则将无法访问核心类,例如java.lang.String、java.lang.Thread、java.util.Array List或java.io.Input Stream以及java API中的所有其他类。
自然,我们在查阅源码时,会发现print和println源码在rt.jar文件下(jdk8及以前版本)。
那么上文中我们提到了,在jdk9版本时,由于对java运行时做了重构,已经删除了rt.jar包,那我们该在哪里去追溯print和println的源码呢?
在jdk9时,JDK又引入了模块(Module)。关于模块的详细了解,我们可以查看:模块 - 廖雪峰的官方网站
以eclipse为例,我们可以在JRE System Library文件下的java.base模块中的java.io包下找到PrintStream.class文件
关于PrintStream类,我们这里对System.out.println()语句进行分析,System类中的out字段是标准输出流,而out字段又是static PrintStream类型的,而PrintStream类又有print和println函数,其重载参数有为Object的。
查阅API文档(JDK17):print
打印一个对象。通过调用String类的valueOf()方法获取打印对象的字符串值,最后根据平台的默认字符编码转换为字节数组,这些字节完全按照write(int)方法的方式写入。
查阅API文档(JDK17):println
打印 Object,然后终止该行。此方法首先调用 String.valueOf(x) 获取打印对象的字符串值,然后的行为如同先调用 print(String)
再调用 println()一样,至于原因,查看下面源码,即能理解。
查看PrintStream源码
查看print(Stirng)和println()源码:
pringtln()源码中的newLine()方法也是在java.base模块下的java.io包下的PrintStream类中
查阅API文档:newLine()方法
查阅API文档后,显而易见,就是写入一个换行符。换行符字符串由系统属性 line.separator 定义,并且换行符不一定是单个换行符('\n')字符。
对println(Object x)再度剖析,查看getClass()源码
从源码文档注释中,我们可以看出该方法的主要作用是返回此 Object 的运行时类,返回的类对象是被表示类的static synchronized方法锁定的对象。不可以重写,要调用的话,一般和getName( )联合使用。
扩展:类加载的第一阶段就是将.class文件加载到内存,并生成一个java.lang.Class对象的过程。getClass( )方法就是获取这个对象,这是当前类的对象在运行时类的所有信息的集合。这个方法也是三种反射方式之一。
反射三种方式:
- 对象的getClass();
- 类名.class;
- Class.forName();
关于write()方法查阅API文档(JDK8)
write()系列方法进行写操作时并不一定直接将所写的内容写出,而先将需要写出的内容放到输出缓冲区,直到缓冲区满、调用flush()方法刷新流或调用close()方法关闭流时才真正输出。这样处理可以减少实际的写出次数,提高系统效率。如果需要写出的内容立即输出,需要在完成write()方法后调用flush()方法刷新流,否则程序可能不能正常工作。
查看print和println源码,会发现都调用了String类提供的valueOf方法
查阅API文档,找到String类下的valueOf方法:
返回Object参数的字符串表示形式。
如果参数为空,则返回null,否则调用obj.toString()方法,并返回toString方法的返回值。
查看String类下的valueOf方法(在java.lang包下)源码:
即当valueOf()方法参数为空时,则返回"null"字符串,否则调用Object类的toString()方法后,返回。
到了这一步后,想必也就明白了为什么输出java对象时,会自动调用toString()方法.
想必大家都应该知道,Object类是所有java类的父类,即超类。因此,所有的Java对象都可以调用Object类提供的方法。toString()方法便是Object类中的方法。到这里,大家应该就明白上文中valueOf()方法源码中对象obj为什么能调用toString()方法了。
继续查看Object类中toString方法的源码:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
通过查看Object类中toString方法的源码,会发现该方法返回的是一个字符串。该字符串由类名(对象是该类的一个实例)、符号标记符“@”和此对象哈希码的无符号十六进制表示组成。
Java中预定义的类
在使用java开发应用程序,我们将频繁的使用到Java类库里的类来开发程序。下面介绍一个典型的类——Date和LocalDate
在jdk8以前(不包括jdk8),采用Date类来表示日期和时间,而在jdk8版本中,新引入了LocalDate类来表示日期和时间。
可以使用Date类中的无参构造方法为当前的日期和时间创建一个实例。它的getTime()方法返回从GMT时间1970年1月1日至今流逝的时间。它的toString()方法返回日期和时间的字符串。Date类还有另外一个构造方法Date(long elapseTime),可以用它创建一个Date对象。
Date类实践代码:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date; //导入日期类工具包
/**
* 在jdk1.8以前,用Date表示日期及时间
* 在jdk1.8以后提供了时间库
*/
public class DateDemo {
public static void main(String[] args) throws ParseException {
//为当前时间创建Date对象
Date date = new Date();
//Date覆盖了toString()
System.out.println(date);
/**
* 可以使用SimpleDateFormat类对Date进行格式化及解析 2022-12-3 22:45:23
* pattern:格式化字符串
* y:year yyyy:四位的年
* M:month MM:两位的月
* d:date dd:两位的日
*
* h:12小时制的小时
* H:24小时制的小时
* m:minute(分钟数)
* s:second(秒数)
* S: milliseconds(毫秒数)
*/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");//1s = 1000 ms
System.out.println(sdf.format(date));
System.out.println(sdf2.format(date));
//将满足sdf格式的字符串转换成(解析)成date对象
String time = "2012-12-12 12:12:12:231";
/*
* 为什么main方法需要抛出异常(ParseException)?
* 因为time这个字符串有可能是用户提供的,是不满足格式要求的,解析不了,需要抛出异常
*/
Date date1 = sdf2.parse(time);
System.out.println("自 1970-01-01 00:00:00 至" + date1 + "流逝的时间毫秒数为:" + date1.getTime()); //getTime() : 返回自 1970-01-01 00:00:00 至date1流逝的时间毫秒数
System.out.println(date1.before(date)); //比较date1是否在date之前 之前:true 之后:false
System.out.println(date1.after(date)); //比较date1是否在date之后 之后:false 之前:true
System.out.println(date.compareTo(date1)); //date > date1 返回真整数 ; date == date1 return 0 ; date < date1 返回负整数
/*
* 什么是时间戳
* 时间戳是指格林威治时间自1970年1月1日(00:00:00 GMT)至当前时间的总秒数。
* 它也被称为Unix时间戳(Unix Timestamp)。
* 通俗的讲,时间戳是一份能够表示一份数据在一个特定时间点已经存在的完整的可验证的数据。
*/
Date date2 = new Date(date1.getTime()); //传入一个时间戳
System.out.println(date2);
}
}
运行结果:
在上文Date代码实践中,我们发现调用Date含参构造方法时,传入了一个时间戳,什么是时间戳?
这里我们需要了解关于epoch time(时间纪元)的相关概念,Unix 时间戳是一种时间表示方式,定义为从格林尼治时间 1970年01月01日 00时00分00秒 起至现在的总秒数,不考虑闰秒。
一个小时表示为UNIX时间戳格式为:3600秒;一天表示为UNIX时间戳为86400秒,闰秒不计算。
补充:时区
在无线电技术之前的时代,为了确定时间,在很多时候只能根据日出、星象等来确定。因此不同的地区形成了不同的历法,但是无论那种历法,地球公转的时长和次数不会改变。历法、日期都只是一个时间的表现形式。
由于地球上每个地区所处的经度不一样,日出时间都不同,便产生了时刻的差异,简称:时差。
比如北京早上日出的时候,可能乌鲁木齐天还没亮。这样就形成了时差。而在全世界人们的认知过程中,一天24小时一个整体,都是从午夜开始。但是时差又确实存在,那么在无线电产生了之后,为了统一协调,1863年,首次使用时区的概念。时区通过设立一个区域的标准时间部分地解决了这个问题。
时区将全世界分为24个区域。每个时区相隔1小时。以格林尼治时间为参照。
北京所在的位置是东八区,比格林尼治时间早了8小时。林尼治时间:1970年1月1日0时0分0秒
(时区转换后)东八区:1980年1月1日8时0分0秒 (SimpleDateFormat内部会从操作系统中获取当前的时区进行转换)
留下一个小思考:为什么Date含参构造传入一个时间戳之后,输出date2对象,会得到一个关于时间的字符串?
Date类的实例有一个状态,即特定的时间点。尽管在使用Date类时不必知道这一点,但时间是用距离一个固定时间点的毫秒数(可正可负)表示的,这个时间点就是所谓的纪元( epoch),它是UTC时间1970年1月1日00:00:00。UTC就是Coordinated Universal Time(国际协调时间),与大家熟悉的GMT(即Greenwich Mean Time,格林尼治时间)一样,是一种实用的科学标准时间。
但是,Date类对于处理人类记录日期的日历信息并不是很有用,如“December 31,1999”。这种特定的日期描述遵循了世界上大多数地区使用的Gregorian 阳历。但是,同样的这个时间点采用中国或希伯来的阴历来描述就很不一样了,倘若我们有来自火星的顾客,基于他们使用的火星历来描述这个时间点就更不一样了。
类库设计者决定将保存时间与给时间点命名分开。所以标准Java类库分别包含了两个类:一个是用来表示时间点的Date类;另一个是用大家熟悉的日历表示法表示日期的LocalDate类。Java 8引入了另外一些类来处理日期和时间的不同方面。
将时间度量与日历分开是一种很好的面向对象设计。通常,最好使用不同的类表示不同的概念。
jdk8以后使用LocalDate表示日期
//jdk8以后使用LocalDate表示日期
LocalDate nowDate = LocalDate.now();
System.out.println(nowDate.toString());
运行结果:
创建指定日期
//创建指定日期
LocalDate localDate = LocalDate.of(1937,9,18);
System.out.println(localDate.atStartOfDay());
运行结果:
Date转LocalDate
//Date转LocalDate
Instant instant = date.toInstant();
LocalDate date3 = LocalDate.ofInstant(instant, ZoneId.systemDefault()); // ZoneId.systemDefault() 转换成系统默认时区
System.out.println(date3);
运行结果:
将日期格式字符串转成LocalDate
//将日期格式字符串转成LocalDate
String token = "1945-08-15";
LocalDate date4 = LocalDate.parse(token);
System.out.println(date4);
运行结果:
将LocalDate再转换成字符串
//将LocalDate再转换成字符串
System.out.println(date4.toString());
运行结果:
表示日期当前时间-------->LocalDateTime
//表示日期当前时间-------->LocalDateTime
LocalDateTime ldt = LocalDateTime.now(); //当前时间
System.out.println(ldt);
运行结果:
Random类
前面我们经常使用Math.random()来获取一个0.0到1.0(不包括1.0)之间的随机double型值。另一种产生随机数的方法是使用java.uitl.Random类,它可以产生一个int、long、double、float和boolean型值。创建一个Random对象时,必须指定一个种子或者使用默认的种子。种子是一个用于初始化一个随机生成器的数字。无参构造方法使用当前已经流逝的时间作为种子,创建一个Random对象。如果两个Random对象有相同的种子,那么它们将产生相同的数列(软件测试中非常有用)。
使用默认种子构建Random对象
import java.util.Random;
public class RandomDemo {
public static void main(String[] args) {
//使用默认种子构建Random对象
Random random = new Random();
System.out.println("使用random生成一个随机int值:" + random.nextInt());
System.out.println("使用random生成一个0~100之间的随机int值:" + random.nextInt(100)); //这里是nexInt()方法重载
//long、double、float和boolean
System.out.println("使用random生成一个随机long值:" + random.nextLong());
System.out.println("使用random生成一个随机double值:" + random.nextDouble());
System.out.println("使用random生成一个随机floate值:" + random.nextFloat());
System.out.println("使用random生成一个随机boolean值:" + random.nextBoolean());
}
}
运行结果:
使用特定种子构造Random,如果random实例的种子相同,它们生成的随机序列是相同的
//使用特定种子构造Random,如果random实例的种子相同,它们生成的随机序列是相同的
Random random1 = new Random(10);
Random random2 = new Random(10);
for(int i =0; i< 10; i++)
System.out.print(random1.nextInt(1000) + " ");
System.out.println();
for(int i =0; i< 10; i++)
System.out.print(random2.nextInt(1000) + " ");
运行结果:
思考:上文提到了随机数种子,那么什么是随机数种子?
随机种子(Random Seed)是计算机专业术语,一种以随机数作为对象的以真随机数(种子)为初始条件的随机数。一般计算机的随机数都是伪随机数,以一个真随机数(种子)作为初始条件,然后用一定的算法不停迭代产生随机数。
详细了解可参考:随机种子的详解_木禾DING的博客-CSDN博客_随机种子
static 静态变量
public class Student{
int id;
static int nextId;
String name;
}
静态变量被类中的所有对象所共享。
Student的实例域id和name称为实例变量。实例变量是绑定到某个特定实例的。它是不能被同一个类的不同对象所共享的。
例如,假设创建了如下两个对象:
Student zhangsan = new Student();
Student lisi = new Student();
zhangsan中的name和lisi中的name是不相关的,它们存储在不同的内存位置。zhangsan中name的变化不会影响lisi中的name,反之亦然。
静态变量也称为类变量,它被该类的所有实例所共享。静态变量将变量值存储在一个公共的内存地址。因为它是公共地址,所以如果某一个对象修改了静态变量的值,那么同一个类的所有对象都会受到影响。java支持静态方法和静态变量,无须创建类的实例就就可以调用静态方法。
静态常量
类中的常量是被该类的所有对象所共享的。因此,常量应该声明为static final,例如,Math类中的常量PI是如下定义的:
pulbic static final double PI = 3.1415926;
静态方法
静态方法是不在对象上执行的方法,比如,Math类的pow方法就是一个静态方法。表达式:Math.pow(a,b);会计算a的b次幂。在完成运算时,它并不使用任何Math对象。
Student类的静态方法不能访问id及name实例字段,因为它不能在对象上执行操作。但是,静态方法可以访问静态字段。
下面是这样一个静态方法的示例:
public static int getNextId() {
return nextId;
}
可以通过类名来调用这个方法:Student.getNextId();
思考:上述方法可以省略关键字static吗?
答案是肯定的。但是,这样一来,你就需要通过Student类对象的引用来调用这个方法。
注:可以使用对象调用静态方法,这是合法的。例如,如果zhangsan是一个 Student对象,可以用zhangsan.getNextId()代替Student.getNextId()。
不过,这种写法很容易造成混淆,其原因是getNextId方法计算的结果与 zhangsan 毫无关系。
我们建议使用类名而不是对象来调用静态方法。
在下面两种情况下可以使用静态方法:
- 方法不需要访问对象状态,因为它需要的所有参数都通过显式参数提供(例如:Math.pow)。
- 方法只需要访问类的静态字段(例如:Student.getNextId)。
初始化块
前面已经讲过两种初始化数据字段的方法:
- 在构造器中设置值
- 在声明中赋值。
实际上,Java还有第三种机制,称为初始化块( initialization block)。
在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行。
在这个示例中,无论使用哪个构造器构造对象,id字段都会在对象初始化块中初始化。首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。
静态代码块
如果类的静态字段需要很复杂的初始化代码,那么可以使用静态的初始化块。
将代码放在一个块中,并标记关键字static。
下面是一个示例: 其功能是将学生ID的起始值赋子一个小于10000的随机整数。
在类第一次加载的时候,将会进行静态字段的初始化。与实例字段一样,除非将静态字段显式地设置成其他值,否则默认的初始值是0、false或null。
所有的静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行:
静态代码块>main>构造代码块>构造器>普通代码块
可见性修饰符
可见性修饰符可以用于确定一个类以及他的成员的可见性。
可以在类、方法、数据域前使用public修饰符,表示他们可以被任何其他类访问。
如果没有使用可见性修饰符,那么默认类、方法和数据域可以被同一个包中的任何一个类访问。这称为包私有(package-private)或包内访问(package-access)。
protected允许子类访问父类中的数据域或方法,但不允许非子类访问这些数据域和方法。
private修饰符限定方法和数据域只能在自己的类中访问。
注意修饰符private只能应用在类成员上,修饰符public可以应用在类或类成员上。
在局部变量上使用public和private都会导致编译错误。
在大多数情况下构造方法都是公共的,但如果想防止用户创建类的实例,就使用私有构造方法,实例化时会提示构造方法不可视错误。
例如Math类所有方法都是静态方法,所以为防止用户创建Math对象,其构造方法定义如下:
private Math(){
}
数据域封装
在前面的程序中,Student类的数据域可以直接修改。这不是一个好的做法,因为:
首先,数据可能被篡改。例如:nextId是用来计算下一个id值的,但是它可能会被错误地设置为一个任意值(例如:Student.nextId = -10).
其次,它使类变得难以维护,同时容易出现错误。假如在其它程序已经使用Student类之后想修改id以确保id是一个非负数。因为使用该类的客户可以直接修改id(例如:peppa.id= -5),所以,不仅要修改Student,而且还要修改使用Student类的这些程序。
为了避免对数据域的直接修改,应该使用private修饰符将数据域声明为私有的,这称为数据域封装。
在定义私有数据域的类外的对象是不能访问这个数据域的。
但是经常会有客户端需要获取、修改数据域的情况。
为了能够访问私有数据域,可以提供一个访问器返回数据域的值(get方法)。
为了更新一个数据域,可以提供一个修改器给数据域设置新值(set方法)。
向方法传递对象参数
给方法传递一个对象,是将对象的引用传递给方法。java只有一种参数传递方式:值传递(pass-by-value)
针对基本数据类型,传递的是基本数据类型值
针对引用数据类型,传递的是引用
堆栈图:
对象数组
数组既可以存储基本数据类型,也可以存储对象。对象的数组实际上是引用变量的数组。
不可变对象和类
通常,创建一个对象后,它的内容是允许之后改变的。
有时需要创建一个一旦创建其内容就不能再改变的对象。称这种对象为一个不可变对象,而它的类称为不可变类。
例如:String类就是不可变的。
要使一个类称为不可变的,它必须满足:
- 所有的数据域都是私有的
- 没有修改器方法
- 没有一个返回指向可变数据域的引用的访问器方法
成员变量VS局部变量
对象的属性就是成员变量。局部变量的声明和使用都在一个方法的内部。
共同点:
1.都是变量,他们的定义形式相同:类型 变量名 = 初始化值;
2.都有作用域:作用域是在一对大括号内
不同点:
1.内存中存放的位置不同:
(1) 成员变量存放在堆空间内
(2) 局部变量存放在栈空间内
2.声明的位置不同(作用域不同):
(1) 成员变量声明在类的内部,方法的外部,作用整个类
(2) 局部变量声明在方法的内部,从它声明的地方开始到包含它最近的块结束
3.初始化值不同 :
(1) 成员变量可以不赋初值,其默认值按照其数据类型来定
(2) 局部变量必须显式地赋初值
4.权限修饰符不同
(1) 成员变量的权限修饰符有四个:
public > protected > [default] > private
(2) 局部变量没有权限修饰符,其访问权限依据其所在的方法而定(与方法的访问权限相同)
注意:如果一个局部变量和一个成员变量具有相同的名字,那么局部变量优先,而同名的类变量将被隐藏。
用var声明局部变量
在Java 10中,如果可以从变量的初始值推导出它们的类型,那么可以用var关键字声明局部变量,而无须指定类型。
Circle circle = new Circle(10);
//用var声明
var circle = new Circle(10); // 这一点很好,因为这样可以避免重复写类型名Circle。
这一点很好,因为这样可以避免重复写类型名Circle。
现在开始,倘若无须了解任何Java API就能从等号右边明显看出类型,在这种情况下我们都将使用var表示法。对Java APl有了更多使用经验后,你可能会希望更多地使用var关键字。
注意:var关键字只能用于方法中的局部变量。参数和数据域的类型必须声明
this引用
关键字this引用对象自身(自身对象的引用)。
this指向调用对象本身的引用名。
可以使用this关键字引用对象的实例成员。
this关键字可以用于引用类的隐藏数据域。
例如,在数据域的set方法中,经常将数据域名用作参数名。在这种情况下,这个数据域在set方法中被隐藏。
为了给它设置新值,需要在方法中引用隐藏的数据名。
隐藏的静态变量可以简单通过"类名.静态变量"的方式引用。隐藏的实例变量就需要使用this来引用。
键字this可以用于调用同一个类的另一个构造方法。
如果一个类有多个构造方法,最好尽可能使用this(参数列表)实现它们。这样做通常可以简化代码,使类易于阅读和维护。