文章目录
- 什么是面向对象
- 面向对象和面向过程区别
- 创建一个对象用什么运算符?
- 面向对象实现伪代码
- 面向对象三大特征
- 类和对象的关系。
- 基础案例
- 代码实现
- 实例化
- 创建car对象时car引用的内存图
- 对象调用方法过程
- 成员变量和局部变量
- 作用范围
- 在内存中的位置
- 关于对象的引用关系
- 简介
- 相关代码
- 内存图解
- 对象相等和引用相等的区别
- 类的构造方法的作用是什么
- 构造方法的特点
- 深拷贝和浅拷贝区别
- 浅拷贝
- 深拷贝
- 匿名对象
- 实例代码
- 匿名对象与实例对象的区别
- 实例代码
- 图解匿名与非匿名内存运行
- 使用场景
- 封装
- 什么是封装
- 什么时private修饰
- 代码示例
- 构造函数
- 什么是构造函数
- 构造函数的小细节
- 构造代码块
- 构造代码块示例以及与构造方法的区别
- this关键字
- 什么是this关键字
- this的应用
- 解决构造函数初始化的问题
- 用于构造函数之间进行互相调用
- static关键字
- 什么是static关键字
- static特点
- 实例变量和类变量的区别
- 静态使用注意事项:
- 静态有利有弊
- 利处
- 弊处
- 错误代码示范
- 图解对象如何调用static变量
- main函数
- 主函数
- 主函数的定义格式以及关键字含义
- 主函数是固定格式的
- 如何使用args
- 静态代码块
- 格式
- 设计优化
- 单例模式
- 简介
- 饿汉式
- 懒汉式(线程不安全)
- 懒汉式(线程安全)
- 内部类模式
- 双重锁校验(线程安全)
- 枚举单例模式(线程安全)
- 相关面试题
- 参考文献
什么是面向对象
面向对象和面向过程区别
面向过程:面向过程是将解决问题的思路转为一个个方法。
面向对象:面向对象则是编写一个对象,将这些思路封装成一个个对象方法,后续调用这个对象解决问题,相对面向过程而言,这种思路更符合人的思维并且更易扩展、复用、维护。
面向对象和面向过程性能差距:人们常常认为面向过程性能高于面向对象,因为创建的对象开销远远大于面向过程,实际上Java面向对象性能差的原因并不是这个,真正的原因是Java为半编译语言,运行并不是直接拿着二进制机械码执行,而是需要结果字节码转换这一步。
而且面向过程的性能并不一定比面向过程快,面向过程也需要计算偏移量以及某些脚本语言的性能也一定比Java好。
创建一个对象用什么运算符?
用new运算符,创建的对象的实例会在堆内存中开辟一个空间。而引用则在栈内存中,指向对象实例。
面向对象实现伪代码
以人开门为例,人需要开门,所以我们需要创建一个门对象,描述门的特征,这个门可以开或者关。所以门的伪代码如下:
门{
开()
{
操作门轴
}
}
上文说到了,面向对象的特点就是符合人的思维,而人开门这个功能,我们就可以创建一个人的对象,编写一个开门的动作,把门打开。通过这种对象调用对象的方式完成了功能。后续我们需要狗开门,猫开门也只是编写一个方法调用门对象的开的动作。
人 {
开门(门对象){
门.打()
}
}
面向对象三大特征
- 封装
- 继承
- 多态
类和对象的关系。
以生活事务为例,现实生活中的对象:张三 李四。他们都有姓名、性别、学习Java的能力。
所以我们要想通过面向对象思想实现抽象出对象,就得提取共性,编写一个类有姓名、性别、学习Java的能力。
public class Student {
private String name;
private int sex;
public void studyJava(){
System.out.println(this.name+"学习java");
}
}
描述时,这些对象的共性有:姓名,年龄,性别,学习java功能。再将这些分析映射到java中,就是以class定义的类进行展开。
public static void main(String[] args) {
Student zhangsan=new Student();
zhangsan.setName("张三");
zhangsan.studyJava();
Student lisi=new Student();
lisi.setName("李四");
lisi.studyJava();
// 输出结果
// 张三学习java
// 李四学习java
}
而具体对象就是对应java在堆内存中用new建立实体。
基础案例
需求:描述汽车(颜色,轮胎数)。描述事物其实就是在描述事物的属性和行为。
属性对应在类中即变量,行为对应的类中的函数(方法)。
代码实现
public class Car {
//描述颜色
String color = "红色";
//描述轮胎数
int num = 4;
//运行行为。
public void run() {
System.out.println("颜色:"+color + " 轮胎数:" + num);
}
}
实例化
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.run();
}
}
创建car对象时car引用的内存图
对象调用方法过程
首先我们看一段代码,这是一个人类的class类代码
public class Person {
private String name;
private int age;
private static String country = "cn";
public Person(String name, int age) {
this.name = name;
this.age = age;
}
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 static void showCountry() {
System.out.println("showCountry " + country);
}
public void speak() {
System.out.println(this.getName() + " speak");
}
}
假如我们在main中编写这样一段代码,请问在内存中他是如何工作的呢?
public static void main(String[] args) {
Person p = new Person("张三", 18);
p.setName("李四");
}
我们先从类类加载时开始分析,由于static关键字修改的变量或者方法会随着jvm加载类时一起创建,所以country
和showCountry()
在方法区是这样的。
然后main方法开始执行对应代码,首先main方法入栈,初始化一个p引用
堆区开辟一个空间,创建一个person实例,p引用指向这个内存空间
调用setName,setName入栈,完成name值修改之后销毁
成员变量和局部变量
作用范围
成员变量作用于整个类中。
局部变量变量作用于函数中,或者语句中。
在内存中的位置
成员变量:在堆内存中,因为对象的存在,才在内存中存在。
局部变量:存在栈内存中。
关于对象的引用关系
简介
对象引用用于指向0个或者多个对象实例,对象实例可以被多个对象引用指向。
相关代码
假如我们使用上文car类执行以下代码,那么在内存中会如何执行呢?
car c=new car();
c.num=5;
car c1=c;
c.run();
内存图解
- 首先堆区开辟一个空间创建car对象,初始化值
- 修改num为5
- c1引用指向c,如下图所示
对象相等和引用相等的区别
- 对象相等:两个对象内存中的存放的内容都相等
- 引用相等:两个引用指向的内存地址相等。
类的构造方法的作用是什么
完成对象初始化,首先在堆区创建对象实例。
构造方法的特点
- 与类名相同
- 无返回值
- 生成对象时自动执行
- 不可重写可以重载
深拷贝和浅拷贝区别
浅拷贝
对象进行拷贝时,如果内部有引用类型,克隆对象仅仅是复制被克隆内部对象的引用地址
为了介绍浅拷贝我们贴出这样一段代码,可以看到一个学生类有id和name,以及一个Vector的引用对象
public class Student implements Cloneable {
private String id;
private String name;
private Vector<String> vector;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Vector<String> getVector() {
return vector;
}
public void setVector(Vector<String> vector) {
this.vector = vector;
}
public Student() {
try {
System.out.println("创建对象需要三秒......");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public Student newInstance() {
try {
return (Student) this.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
然后我们使用下面这段代码进行测试,可以看到输出结果为true,说明student2的vector是student1的。如下图所示,克隆对象的内部引用对象和student1是通用的
@Test
public void cloneTest() throws CloneNotSupportedException {
long start,end;
start=System.currentTimeMillis();
Student student=new Student();
end=System.currentTimeMillis();
System.out.println("学生1创建时间长 "+(end-start));
student.setId("1");
student.setName("小明");
Vector<String> v = new Vector<>();
v.add("000000");
v.add("000001");
student.setVector(v);
start=System.currentTimeMillis();
Student student2= student.newInstance();
end=System.currentTimeMillis();
System.out.println("学生2创建时间长 "+(end-start));
for (String s : student2.getVector()) {
System.out.println(s);
}
// false则说明深拷贝成功
System.out.println(student.getVector()==student2.getVector());
}
深拷贝
了解了浅拷贝之后,我们就可以解释深拷贝了,克隆对象的内部引用对象都是全新复制出来的一份
基于上文student代码我们对此进行改造,重写以下clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
Student clone = new Student();
clone.setId(this.getId());
clone.setName(this.getName());
//避免clone导致浅拷贝问题
Vector<String> srcVector = this.getVector();
Vector<String> dstVector = new Vector<>();
for (String v : srcVector) {
dstVector.add(v);
}
clone.setVector(dstVector);
return clone;
}
匿名对象
实例代码
如下所示,在堆区创建一个对象实例,用后即被销毁。为了介绍匿名对象,我们首先需要编写一个汽车类
public class Car {
//描述颜色
private String color = "红色";
//描述轮胎数
private int num = 4;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
//运行行为。
public void run() {
this.setNum(++num);
System.out.println("颜色:" + color + " 轮胎数:" + num);
}
}
然后我们使用测试单元进行测试
@Test
public void anonymously(){
new Car().run();
}
上述代码的工作过程如下所示,可以看到完成方法调用之后
匿名对象与实例对象的区别
实例代码
可以看到我们现实创建一个非匿名的汽车类和匿名汽车类,并作为show方法的参数传入
public static void main(String[] args) {
Car car = new Car();
show(car);
show(new Car());
/**
* 输出结果
* 颜色:black 轮胎数:4
* 颜色:black 轮胎数:4
*/
}
public static void show(Car c) {
c.setNum(3);
c.setColor("black");
c.run();
}
图解匿名与非匿名内存运行
非匿名对象内存运行过程图解
匿名对象完成方法调用后即被销毁
使用场景
-
当对对象的方法只调用一次时,可以用匿名对象来完成,这样写比较简化。如果对一个对象进行多个成员调用,必须给这个对象起个名字。
-
可以将匿名对象作为实际参数进行传递。
封装
什么是封装
以生活为例子,某公司老板招开发人员,招得开发人员后,开发人员工作过程不用看到,老板只关注开发结果,而老板只看到开发结果这一现象即封装。
什么时private修饰
private :私有,权限修饰符:用于修饰类中的成员(成员变量,成员函数)。私有只在本类中有效。
代码示例
如下所示,setAge就是对age赋值的封装,隐藏对年龄操作的细节,用户只需通过这个方法完成自己需要的赋值动作即可
public class Person {
private int age;
public void setAge(int a) {
if (a > 0 && a < 130) {
age = a;
speak();
} else
System.out.println("feifa age");
}
public int getAge() {
return age;
}
private void speak() {
System.out.println("age=" + age);
}
}
构造函数
什么是构造函数
- 对象一建立就会调用与之对应的构造函数。
- 构造函数的作用:可以用于给对象进行初始化。
构造函数的小细节
- 类默认有构造函数,显示创建后默认构造类就消失。
- 默认构造函数权限和类权限修饰符一致,例如类权限为public,则构造方法默认也为public,除非显示修改权限。
构造代码块
构造代码块示例以及与构造方法的区别
构造代码块。
作用:给对象进行初始化。
对象一建立就运行,而且优先于构造函数执行。
和构造函数的区别:
1. 构造代码块是给所有对象进行统一初始化,在jvm完成类加载时就会运行方法,也就是说调用静态方法的情况下,构造代码块也会被执行
2. 而构造函数是给对应的对象初始化。
构造代码块会随着对象实例的创建和运行。
public class Person {
private String name;
private int age;
{
System.out.println("person 类的构造代码块执行了");
run();
}
public void run(){
System.out.println("person run");
}
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;
}
}
如下便是main函数的运行结果
public static void main(String[] args) {
Person p=new Person();
/**
* person 类的构造代码块执行了
* person run
*/
}
this关键字
什么是this关键字
代表它所在函数所属对象的引用。简单来说,调用对象方法的对象就是this关键字多代表的对象。
this的应用
解决构造函数初始化的问题
如下代码,假如所有成员变量不加this,编译器则不会找成员变量name,导致赋值过程毫无意义。
输出结果
对此我们就可以使用this关键字即可解决问题
public class Person {
private String name;
private int age;
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;
}
}
用于构造函数之间进行互相调用
注意:this语句只能定义在构造函数的第一行。
public Person(String name, int age) {
this(name);
this.age = age;
}
public Person(String name) {
this.name = name;
}
static关键字
什么是static关键字
用法:是一个修饰符,用于修饰成员(成员变量,成员函数).
当成员被静态修饰后,就多了一个调用方式,除了可以被对象调用外,还可以直接被类名调用。类名.静态成员。
static特点
- 随着类的加载而加载。随着jvm完成类加载该变量或者方法就会被加载到方法区。
- 静态会随着类的消失而消失。说明它的生命周期最长。
- 优先于对象存在,即静态变量先存在,对象后存在。
- 被所有对象所共享,所以有时候我们需要考虑线程安全问题。
- 可以直接被类名所调用。
实例变量和类变量的区别
- 实例变量随着对象的创建而存放在堆内存上,而类变量即静态变量随着类加载而存放在方法区上。
- 实例变量随着对象实例消亡而消亡,而类变量随着类的消亡和消亡。
静态使用注意事项:
- 静态方法只能访问静态变量,实例对象则静态非静态都可以访问
- 静态方法不可使用this和super关键字,因为this和super都是对象实例的关键字,this关键字是指向对象实例,static关键字在类加载时候就能被指向,故不可使用这两个关键字。
静态有利有弊
利处
随着类加载而创建,每个对象公用一份,无需每个实例到堆区创建一份。
弊处
- 生命周期长
- 使用不当可能造成线程安全问题。
- 访问有局限性,静态方法只能访问静态相关变量或者方法。
错误代码示范
图解对象如何调用static变量
我们首先编写这样一段代码
public class Person {
private String name;
private int age;
public static int staticVar=4;
}
然后我们的main方法进行这样的调用,在jvm内存是如何执行的呢?
public static void main(String[] args) {
Person person=new Person();
person.staticVar=5;
}
- main方法入栈
- 堆区创建person对象实例,p指向实例
- p实例通过堆区对象操作方法的静态变量,修改值为5
main函数
主函数
主函数是一个特殊的函数,程序执行的入口,可以被jvm执行。
主函数的定义格式以及关键字含义
public:代表着该函数访问权限是最大的。
static:代表主函数随着类的加载就已经存在了。
void:主函数没有具体的返回值。
main:不是关键字,但是是一个特殊的单词,可以被jvm识别。
(String[] args):函数的参数,参数类型是一个数组,该数组中的元素是字符串。字符串类型的数组。
主函数是固定格式的
只有符合上述的固定格式,jvm才能识别。
jvm在调用主函数时,传入的是new String[0];即可长度为0的String
如何使用args
class MainDemo
{
public static void main(String[] args)//new String[]
{
String[] arr = {"hah","hhe","heihei","xixi","hiahia"};
MainTest.main(arr);
}
}
class MainTest
{
public static void main(String[] args)
{
for(int x=0; x<args.length; x++)
System.out.println(args[x]);
}
}
静态代码块
格式
static
{
静态代码块中的执行语句。
}
我们在person类中编写一个静态代码块,然后调用其他静态方法,可以发现静态代码块会随着类的加载而完成执行,并且只执行一次
public class Person {
static {
System.out.println("静态代码块,随着方法执行而执行.....");
}
public static void func(){
System.out.println("静态方法执行了");
}
}
main方法调用示例
public static void main(String[] args) {
Person.func();
Person.func();
/**
* 输出结果
* 静态代码块,随着方法执行而执行.....
* 静态方法执行了
* 静态方法执行了
*/
}
设计优化
单例模式
简介
对于重量级对象的创建可能会导致以下问题:
- 创建对象开销大
- GC压力大,可能导致系统卡顿
饿汉式
代码如下所示,可以看到对象随着类的加载就会立刻完成创建,这就导致假如我们使用这个类的某些不需要单例的方法也会完成对象的创建。
例如我们就像调用以下Singleton 的sayHello这个静态方法就会导致单例实例被创建,所以如果非必要我们不建议采用这种非延迟加载的单例模式
public class Singleton {
private Singleton() {
System.out.println("创建单例");
}
public static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
public static void sayHello(){
System.out.println("hello");
}
}
测试代码,可以看到调用静态方法单例就被被创建了
public static void main(String[] args) {
Singleton.sayHello();
/**
* 输出:
* 创建单例
* hello
*/
}
原理也很简单,静态变量和方法都在方法区,随着类被加载这些变量或者方法都会被加载。
懒汉式(线程不安全)
懒汉式即实现延迟加载的有效手段,代码如下所示
/**
* 延迟加载的单例类 避免jvm加载时创建对象
*/
public class LazySingleton {
private LazySingleton() {
System.out.println("懒加载单例类");
}
private static LazySingleton instance = null;
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
public static void sayHello(){
System.out.println("hello");
}
}
调用示例如下所示,可以看到静态方法调用后并没有创建实例,只有调用获取对象时才会得到对象实例
public static void main(String[] args) {
LazySingleton.sayHello();
LazySingleton.getInstance();
/**
* 输出结果
* hello
* 懒加载单例类
*/
}
懒汉式的工作原理如下图所示,可以看到只有调用getInstance后,才会在堆内存中开辟一块内存空间创建对象
实际上,当前的懒汉式存在线程安全问题,如上内存图解所示,可能会有两个线程走到==null的判断中进而出现创建多个单例对象的情况。我们使用JUC的倒计时门闩调用获取单例的情况,可以看到对象被创建了多次。
/**
* 不加synchronized的懒加载 加上则没有下面这样输出结果
*/
@Test
public void threadTest0(){
ExecutorService threadPool = Executors.newFixedThreadPool(1000);
CountDownLatch countDownLatch=new CountDownLatch(1);
for (int i = 0; i < 10000; i++) {
threadPool.submit(()->{
System.out.println(LazySingleton.getInstance());
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
countDownLatch.countDown();
/**
* 懒加载单例类
* 懒加载单例类
* 懒加载单例类
* 懒加载单例类
* com.optimize.design.LazySingleton@12423874
* com.optimize.design.LazySingleton@350c55ec
* com.optimize.design.LazySingleton@350c55ec
* com.optimize.design.LazySingleton@350c55ec
* 懒加载单例类
* com.optimize.design.LazySingleton@350c55ec
* com.optimize.design.LazySingleton@5897fc07
* 懒加载单例类
* com.optimize.design.LazySingleton@5897fc07
* 懒加载单例类
* com.optimize.design.LazySingleton@39d8305
* com.optimize.design.LazySingleton@1a0eae7f
* com.optimize.design.LazySingleton@1a0eae7f
* 懒加载单例类
* com.optimize.design.LazySingleton@1a0eae7f
* 懒加载单例类
*/
}
懒汉式(线程安全)
要想实现线程安全,我们只需要通过下面这种方式上锁即可保线程安全,但是缺点也很明显,在高并发情况下,获取对象的实践会随着增加
/**
* 增加 synchronized确保线程安全
* @return
*/
public synchronized static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
测试用例如下,可以看到饿汉式和线程安全懒汉式时间的差距
@Test
public void test(){
long start=System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
Singleton.getInstance();
}
long end=System.currentTimeMillis();
System.out.println(end-start);
start=System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
LazySingleton.getInstance();
}
end=System.currentTimeMillis();
System.out.println(end-start);
/**
* 输出结果
*
* 创建单例
* 3
* 懒加载单例类
* 20
*/
}
内部类模式
上文提到的懒汉式的性能问题,所以我们可以使用内部类模式解决该问题,代码如下所示,可以看到我们在单例类的内部增加一个静态内部类,该类被加载时静态内部类并不会被加载,只有调用getInstance才会创建单例对象,并且该对象的创建是随着类的加载就完成创建,故这是一种线程友好的单例模式
/**
* 线程安全的单例 但还是会被反射攻破
*/
public class StaticSingleton {
private StaticSingleton() {
System.out.println("静态内部类延迟加载");
}
private static class SingletonHolder {
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance(){
return SingletonHolder.instance;
}
public static void sayHello(){
System.out.println("hello");
}
}
测试代码和输出结果
public static void main(String[] args) {
StaticSingleton.sayHello();
StaticSingleton.getInstance();
/**
* 输出结果
*
* hello
* 静态内部类延迟加载
*/
}
内部类单例模式工作过程
实际上这种模式也有缺点,就是会被发射攻破,后续我们会介绍对应的解决方案
双重锁校验(线程安全)
双重锁校验的单例模式如下所示,可以看到双重锁校验的编码方式和简单,第一次判断避免没必要的执行,第二次判断避免第一次判定为空走到创建对象代码块的线程,从而避免线程安全问题
public class DoubleCheckLockSingleton {
private static DoubleCheckLockSingleton instance = null;
private DoubleCheckLockSingleton() {
System.out.println("双重锁单例对象被创建");
}
public static DoubleCheckLockSingleton getInstance() {
if (instance != null) {
return instance;
}
synchronized (DoubleCheckLockSingleton.class) {
//这一重校验是为了避免上面判空后进入休眠走到这个代码块的线程
if (null == instance) {
instance = new DoubleCheckLockSingleton();
return instance;
}
}
return instance;
}
}
性能上我们可以看到双重锁校验的性能要好于静态内部类的方式
@Test
public void test(){
long start=System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
Singleton.getInstance();
}
long end=System.currentTimeMillis();
System.out.println(end-start);
start=System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
LazySingleton.getInstance();
}
end=System.currentTimeMillis();
System.out.println(end-start);
start=System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
StaticSingleton.getInstance();
}
end=System.currentTimeMillis();
System.out.println(end-start);
start=System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
DoubleCheckLockSingleton.getInstance();
}
end=System.currentTimeMillis();
System.out.println(end-start);
/**
* 创建单例
* 6
* 懒加载单例类
* 20
* 静态内部类延迟加载
* 4
* 双重锁单例对象被创建
* 3
*/
}
枚举单例模式(线程安全)
/**
* 使用枚举保证类单例
*/
public enum Elvis {
INSTANCE;
private String name="elvis";
public String getName() {
return name;
}
public static Elvis getInstance(){
return INSTANCE;
}
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
}
可以看到这种方式不会被反射攻破
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Class<Elvis> elvisClass = Elvis.class;
Elvis elvis1 = elvisClass.newInstance();
System.out.println(elvis1.getName());
}
输出结果
相关面试题
下面这段代码。new StaticCode(4)的输出结果?
public class StaticCode {
int num = 9;
StaticCode() {
System.out.println("b");
}
static {
System.out.println("a");
}
{
System.out.println("c" + this.num);
}
StaticCode(int x) {
System.out.println("d");
}
public static void show() {
System.out.println("show run");
}
}
答案:a c9 d
创建类,加载顺序为:
- 加载静态代码块
- 加载构造代码块
- 加载构造方法
参考文献
Java基础常见面试题总结(中)
Effective Java中文版(第3版)
Java系统性能优化实战