
- 词 义
- 目标
- 拼 音
- duì xìang
- 出 自
- 《套不住的手》
- 中文名
- 对象
-
2022-05-13 16:11:54
一:基础概念
1.1 面向过程
“面向过程”(Procedure Oriented)是一种以过程为中心的编程思想。这些都是以什么正在发生为主要目标进行编程,不同于面向对象的是谁在受影响。与面向对象明显的不同就是封装、继承、类。简写为POP。
1.2 面向对象
面向对象编程(Object Oriented Programming,OOP,面向对象程序设计)的主要思想是把构成问题的各个事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙一个事物在整个解决问题的步骤中的行为。
1.3 基于对象
基于对象的编程语言没有提供象抽象、继承、重载等有关面向对象语言的许多功能。而是把其它语言所创建的复杂对象统一起来,从而形成一个非常强大的对象系统,以供使用。
二:三者对比对比
面向过程 面向对象 基于对象 产生时间 1965 1967 - 代表语言 C C++,JAVA BASIC(VB4-VB6) 基本特征 自顶向下,逐步求精 抽象、封装、继承、多态 封装 性能 好 较差 较好 维护难度 巨大 容易 较大 适用程序 一般用于小型 大型 - 三:具体例子
有人这么形容OP和OO的不同:用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭
盖浇饭的好处就是“菜”“饭”分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。
用软件工程的专业术语就是“可维护性”比较好,“饭”和“菜”的耦合度比较低。
蛋炒饭将“蛋”“饭”搅和在一起,想换“蛋”“饭”中任何一种都很困难,耦合度很高,以至于“可维护性”比较差。
更多相关内容 -
JavaScript 进阶教程(1)--面向对象编程
2020-08-16 18:13:371 学习目标 理解面向对象开发思想 掌握 JavaScript 面向对象开发相关模式 ...当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。 (2) 对象是一个.目录
1 学习目标
-
理解面向对象开发思想
-
掌握 JavaScript 面向对象开发相关模式
2 面向对象介绍
2.1 什么是对象
Everything is object (一切皆对象)
我们可以从两个层次来理解对象:
(1) 对象是单个事物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
(2) 对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集。ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。 严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都 映射到一个值。
提示:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。
2.2 什么是面向对象
面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。 它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。 因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
面向对象与面向过程区别:
-
面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊。
-
面向对象就是找一个对象,指挥得结果。
-
面向对象将执行者转变成指挥者。
-
面向对象不是面向过程的替代,而是面向过程的封装。
面向对象的特性:
-
封装性
-
继承性
-
多态性
扩展阅读:
2.3 JavaScript 中面向对象的基本体现
在 JavaScript 中,所有数据类型都可以视为对象,当然也可以自定义对象。 自定义的对象数据类型就是面向对象中的类( Class )的概念。
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:
var std1 = { name: '张三', score: 98 } var std2 = { name: '李四', score: 81 }
而处理学生成绩可以通过函数实现,比如打印学生的成绩:
function printScore (student) { console.log('姓名:' + student.name + ' ' + '成绩:' + student.score) }
如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程, 而是
Student
这种数据类型应该被视为一个对象,这个对象拥有name
和score
这两个属性(Property)。 如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个printScore
消息,让对象自己把自己的数据打印出来。抽象数据行为模板(Class):
function Student (name, score) { this.name = name this.score = score } Student.prototype.printScore = function () { console.log('姓名:' + this.name + ' ' + '成绩:' + this.score) }
根据模板创建具体实例对象(Instance):
var std1 = new Student('张三', 98) var std2 = new Student('李四', 81)
实例对象具有自己的具体行为(给对象发消息):
std1.printScore() // => 姓名:张三 成绩:98 std2.printScore() // => 姓名:李四 成绩 81
面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。 Class 是一种抽象概念,比如我们定义的 Class——Student ,是指学生这个概念, 而实例(Instance)则是一个个具体的 Student ,比如, 张三 和 李四 是两个具体的 Student 。
面向对象的设计思想是:
-
抽象出 Class
-
根据 Class 创建 Instance
-
指挥 Instance 得结果
面向对象的抽象程度比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。
3 JavaScript 如何创建对象
3.1 字面量方式
我们可以直接通过
new Object()
创建:var person = new Object() person.name = '张三' person.age = 18 person.sayName = function () { console.log(this.name) }
每次创建通过
new Object()
比较麻烦,所以可以通过它的简写形式对象字面量来创建:var person = { name: '张三', age: 18, sayName: function () { console.log(this.name) } }
上面的写法是没有问题的,但是假如我们要生成两个
person
实例对象呢?var person1 = { name: '张三', age: 18, sayName: function () { console.log(this.name) } } var person2 = { name: '李四', age: 16, sayName: function () { console.log(this.name) } }
通过上面的代码我们不难看出,这样写的代码太过冗余,重复性太高。
3.2 简单方式的改进:工厂函数
我们可以写一个函数,解决上边代码重复的问题:
function createPerson (name, age) { return { name: name, age: age, sayName: function () { console.log(this.name) } } }
生成实例对象:
var p1 = createPerson('张三', 18) var p2 = createPerson('李四', 18)
这样封装比上边的方式好多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题, 但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
3.3 更优雅的工厂函数:构造函数
一种更优雅的工厂函数就是下面这样,构造函数:
function Person (name, age) { this.name = name this.age = age this.sayName = function () { console.log(this.name) } } var p1 = new Person('张三', 18) p1.sayName() // => 张三 var p2 = new Person('李四', 23) p2.sayName() // => 李四
在上面的示例中,
Person()
函数取代了createPerson()
函数,但是实现效果是一样的。 这是为什么呢?我们注意到,
Person()
中的代码与createPerson()
有以下几点不同之处:-
没有显示的创建对象
-
直接将属性和方法赋给了
this
对象 -
没有
return
语句 -
函数名使用的是大写的
Person
3.4 构造函数代码执行过程
要创建
Person
实例,则必须使用new
操作符。 以这种方式调用构造函数会经历以下 4 个步骤:-
创建一个新对象。
-
将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)。
-
执行构造函数中的代码。
-
返回新对象。
下面是具体的伪代码:
function Person (name, age) { // 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象 // var instance = {} // 然后让内部的 this 指向 instance 对象 // this = instance // 接下来所有针对 this 的操作实际上操作的就是 instance this.name = name this.age = age this.sayName = function () { console.log(this.name) } // 在函数的结尾处会将 this 返回,也就是 instance // return this }
3.5 构造函数和实例对象的关系
使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。 在每一个实例对象中的_proto_中同时有一个
constructor
属性,该属性指向创建该实例的构造函数:console.log(p1.constructor === Person) // => true console.log(p2.constructor === Person) // => true console.log(p1.constructor === p2.constructor) // => true
对象的
constructor
属性最初是用来标识对象类型的, 但是,如果要检测对象的类型,还是使用instanceof
操作符更可靠一些:console.log(p1 instanceof Person) // => true console.log(p2 instanceof Person) // => true
总结:
1 构造函数是根据具体的事物抽象出来的抽象模板。
2 实例对象是根据抽象的构造函数模板得到的具体实例对象。
3 每一个实例对象都具有一个
constructor
属性,指向创建该实例的构造函数。( 此处constructor
是实例的属性的说法不严谨,具体后面的原型会讲到)4 可以通过实例的
constructor
属性判断实例和构造函数之间的关系。(这种方式不严谨,推荐使用instanceof
操作符,后面学原型会解释为什么)3.6 构造函数的问题
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:
function Person (name, age) { this.name = name this.age = age this.type = '学生' this.sayHello = function () { console.log('hello ' + this.name) } } var p1 = new Person('王五', 18) var p2 = new Person('李四', 16)
上边的代码,从表面看上好像没什么问题,但是实际上这样做,有一个很大的弊端。 那就是对于每一个实例对象,
type
和sayHello
都是一模一样的内容, 每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。console.log(p1.sayHello === p2.sayHello) // => false
对于这种问题我们可以把需要共享的函数定义到构造函数外部:
function sayHello = function () { console.log('hello ' + this.name) } function Person (name, age) { this.name = name this.age = age this.type = '学生' this.sayHello = sayHello } var p1 = new Person('王五', 18) var p2 = new Person('李四', 16) console.log(p1.sayHello === p2.sayHello) // => true
这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。如何解决这个问题呢?你肯定想到了可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:
var fns = { sayHello: function () { console.log('hello ' + this.name) }, sayAge: function () { console.log(this.age) } } function Person (name, age) { this.name = name this.age = age this.type = '学生' this.sayHello = fns.sayHello this.sayAge = fns.sayAge } var p1 = new Person('王五', 18) var p2 = new Person('李四', 16) console.log(p1.sayHello === p2.sayHello) // => true console.log(p1.sayAge === p2.sayAge) // => true
至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。 但是代码看起来还是那么的格格不入,那有没有更好的方式呢?
4 原型
4.1 更好的解决方案:
prototype
Javascript 规定,每一个构造函数都有一个
prototype
属性,指向另一个对象。 这个对象的所有属性和方法,都会被构造函数的实例继承。这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在
prototype
对象上。function Person (name, age) { this.name = name this.age = age } console.log(Person.prototype) Person.prototype.type = '学生' Person.prototype.sayName = function () { console.log(this.name) } var p1 = new Person(...) var p2 = new Person(...) console.log(p1.sayName === p2.sayName) // => true
这时所有实例的
type
属性和sayName()
方法, 其实都是同一个内存地址,指向prototype
对象,因此就提高了运行效率。构造函数、实例、原型三者之间的关系:
任何函数都有一个
prototype
属性,该属性是一个对象。function F () {} console.log(F.prototype) // => object F.prototype.sayHi = function () { console.log('hi!') }
构造函数的
prototype
对象默认都有一个constructor
属性,指向prototype
对象所在函数。console.log(F.constructor === F) // => true
通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype
对象的指针__proto__
。var instance = new F() console.log(instance.__proto__ === F.prototype) // => true
`__proto__` 是非标准属性。
实例对象可以直接访问原型对象成员:
instance.sayHi() // => hi!
总结:
-
任何函数都具有一个
prototype
属性,该属性是一个对象。 -
构造函数的
prototype
对象默认都有一个constructor
属性,指向prototype
对象所在函数。 -
通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype
对象的指针__proto__。
-
所有实例都直接或间接继承了原型对象的成员。
4.2 属性成员的搜索原则:原型链
了解了 构造函数-实例-原型对象 三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的成员。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。
-
搜索首先从对象实例本身开始。
-
如果在实例中找到了具有给定名字的属性,则返回该属性的值。
-
如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。
-
如果在原型对象中找到了这个属性,则返回该属性的值。
也就是说,在我们调用
person1.sayName()
的时候,会先后执行两次搜索:-
首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。
-
然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。
-
于是,它就读取那个保存在原型对象中的函数。
-
当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。
这就是多个对象实例共享原型所保存的属性和方法的基本原理。
总结:
-
先在自己身上找,找到即返回。
-
自己身上找不到,则沿着原型链向上查找,找到即返回。
-
如果一直到原型链的末端还没有找到,则返回
undefined。
4.3 实例对象读写原型对象成员
读取:
-
先在自己身上找,找到即返回。
-
自己身上找不到,则沿着原型链向上查找,找到即返回。
-
如果一直到原型链的末端还没有找到,则返回
undefined。
值类型成员写入(
实例对象.值类型成员 = xx
):-
当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上。
-
也就是说该行为实际上会屏蔽掉对原型对象成员的访问。
引用类型成员写入(
实例对象.引用类型成员 = xx
):同上。复杂类型修改(
实例对象.成员.xx = xx
):-
同样会先在自己身上找该成员,如果自己身上找到则直接修改。
-
如果自己身上找不到,则沿着原型链继续查找,如果找到则修改。
-
如果一直到原型链的末端还没有找到该成员,则报错(
实例对象.undefined.xx = xx
)。
4.4 更简单的原型语法
我们注意到,前面例子中每添加一个属性和方法就要敲一遍
Person.prototype
。 为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:function Person (name, age) { this.name = name this.age = age } Person.prototype = { type: '学生', sayHello: function () { console.log('我叫' + this.name + ',我今年' + this.age + '岁了') } }
在该示例中,我们将
Person.prototype
重置到了一个新的对象。 这样做的好处就是为Person.prototype
添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了constructor
成员。所以,我们为了保持
constructor
的指向正确,建议的写法是:function Person (name, age) { this.name = name this.age = age } Person.prototype = { constructor: Person, // => 手动将 constructor 指向正确的构造函数 type: '学生', sayHello: function () { console.log('我叫' + this.name + ',我今年' + this.age + '岁了') } }
4.5 原生对象的原型
所有函数都有 prototype 属性对象。
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- ...
为数组对象和字符串对象扩展原型方法:
//为内置对象添加原型方法 //我们在系统的对象的原型中添加方法,相当于在改变源码 //我希望字符串中有一个倒序字符串的方法 String.prototype.myReverse = function() { for (var i = this.length - 1; i >= 0; i--) { console.log(this[i]); } }; var str = "abcdefg"; str.myReverse(); //为Array内置对象的原型对象中添加方法 Array.prototype.mySort = function() { for (var i = 0; i < this.length - 1; i++) { for (var j = 0; j < this.length - 1 - i; j++) { if (this[j] < this[j + 1]) { var temp = this[j]; this[j] = this[j + 1]; this[j + 1] = temp; } //end if } // end for } //end for }; var arr = [100, 3, 56, 78, 23, 10]; arr.mySort(); console.log(arr); String.prototype.sayHi = function() { console.log(this + "哈哈,我又变帅了"); }; //字符串就有了打招呼的方法 var str2 = "小杨"; str2.sayHi();
4.6 原型对象的一些问题
-
共享数组
-
共享对象
如果真的希望可以被实例对象之间共享和修改这些共享数据那就不是问题。但是如果不希望实例之间共享和修改这些共享数据则会出现问题。一个更好的建议是,最好不要让实例之间互相共享数组或者对象成员,一旦修改的话会导致数据的走向很不明确而且难以维护。
原型对象使用建议:
-
私有成员(一般就是非函数成员)放到构造函数中。
-
共享成员(一般就是函数)放到原型对象中。
-
如果重置了
prototype
记得修正constructor
的指向。
今天的学习就到这里,你可以使用今天学习的技巧来改善一下你曾经的代码,如果想继续提高,欢迎关注我,每天学习进步一点点,就是领先的开始。如果觉得本文对你有帮助的话,欢迎点赞,评论,转发!!!
-
-
Java对象创建过程
2022-03-28 17:54:13java对象创建过程、对象的组成、对象头、实例数据、对齐填充、对象创建方式、new关键字、Class类的newInstance方法、Constructor类的newInstance方法、Object类的clone方法、反序列化、无父类的对象创建、有父类的...文章目录
一、对象的组成
对象的组成包含三部分:对象头、实例数据、对齐填充。
1. 对象头
Java的对象头由以下三部分组成:MarkWord、指向类的指针、数组长度(只有数组对象才有)
① MarkWord
MarkWord包含:哈希码、GC分代年龄、锁标识状态、
线程持有的锁、偏向线程ID(一般占32/64 bit)。MarkWord记录了对象锁相关的信息。
当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和MarkWord有关。
MarkWord在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
32位JVM中,MarkWord在不同的锁状态下存储的内容:
其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
锁升级流程:
- 当对象没有锁时,这就是一个普通的对象,MarkWord记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。锁状态为无锁。
- 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
- 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,MarkWord中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
- 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是MarkWord中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把MarkWord里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
- 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁MarkWord的指针,同时在对象锁MarkWord中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把MarkWord中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
- 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
- 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
② 指向类的指针
Java对象的类数据保存在方法区。
该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
③ 数组长度
只有数组对象保存了这部分数据。
该数据在32位和64位JVM中长度都是32bit。
2. 实例数据
对象的实例数据就是在Java代码中能看到的
属性
和他们的属性值
。3. 对齐填充
因为JVM要求Java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。
二、对象创建方式
1. new关键字
最常见的也是最简单的创建对象的方式,通过这种方式我们可以调用任意的构造函数(无参的和有参的)去创建对象。
public static void main(String[] args) { User = new User(); }
2. Class类的newInstance方法
通过Java的反射机制使用Class类的newInstance方法来创建对象。这个newInstance方法调用无参的构造器创建对象。
public static void main(String[] args) throws Exception { // 方法1 User user1 = (User)Class.forName("com.joker.pojo.User").newInstance(); // 方法2 User user2 = User.class.newInstance(); }
事实上Class的newInstance方法内部调用的是Constructor的newInstance方法。
3. Constructor类的newInstance方法
通过Java的反射机制使用Constructor类的newInstance方法来创建对象。
java.lang.relect.Constructor类里的newInstance方法比Constructor类的newInstance方法更加强大些,我们可以通过这个newInstance方法调用有参数的和私有的构造函数。
public static void main(String[] args) throws Exception { Constructor<User> constructor = User.class.getConstructor(Integer.class); User user3 = constructor.newInstance(123); }
4. Object类的clone方法
通过实现Cloneable接口,重写Object类的clone方法来创建对象(浅拷贝)。
Java为所有对象提供了clone方法(Object类),又出于安全考虑,将它设置为了保护属性。
protected native Object clone() throws CloneNotSupportedException;
我们可以通过反射(reflect)机制在任意对象中调用该方法。
如果不通过反射的方式,我们要如何实现对象克隆呢?可以通过实现Cloneable接口,重写Object类的clone方法来实现对象的克隆。
实现原理:
Java API采用判断是否实现空接口Cloneable的方法来判断对象所属的类是否支持克隆。如果被克隆对象所属类没有实现该接口,则抛出NotDeclareCloneMethod 异常。当支持克隆时,通过重写Object类的clone方法,并把方法的修饰符改为public,就可以直接调用该类的实例对象的clone方法实现克隆。我们常用的很多类都是通过这种方式来实现的,如:ArrayList、HashMap等。
@Data public class User implements Cloneable { private String id; private String userName; @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } public static void main(String[] args) throws CloneNotSupportedException { User user = new User(); User user1 = (User)user.clone(); } }
5. 反序列化
当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。
public static void main(String[] args) throws Exception { User user = new User(); user.setId("1"); user.setUserName("haha"); // 写对象 ObjectOutputStream output = new ObjectOutputStream( new FileOutputStream("F:\\joker\\text.txt")); output.writeObject(user); output.close(); // 读对象 ObjectInputStream input = new ObjectInputStream(new FileInputStream( "F:\\joker\\text.txt")); User user1 = (User) input.readObject(); }
三、对象创建过程
这里以new关键字方式创建对象为例。
对象创建过程分为以下几步:
-
检查类是否已经被加载;
new关键字时创建对象时,首先会去运行时常量池中查找该引用所指向的类有没有被虚拟机加载,如果没有被加载,那么会进行类的加载过程。类的加载过程需要经历:加载、链接、初始化三个阶段。
具体过程可参考文章:Java类的加载机制
-
为对象分配内存空间;
此时,对象所属类已经加载,现在需要在堆内存中为该对象分配一定的空间,该空间的大小在类加载完成时就已经确定下来了。
为对象分配内存空间有两种方式:
- 第一种是jvm将堆区抽象为两块区域,一块是已经被其他对象占用的区域,另一块是空白区域,中间通过一个指针进行标注,这时只需要将指针向空白区域移动相应大小空间,就完成了内存的分配,当然这种划分的方式要求虚拟机的对内存是地址连续的,且虚拟机带有内存压缩机制,可以在内存分配完成时压缩内存,形成连续地址空间,这种分配内存方式成为“指针碰撞”,但是很明显,这种方式也存在一个比较严重的问题,那就是多线程创建对象时,会导致指针划分不一致的问题,例如A线程刚刚将指针移动到新位置,但是B线程之前读取到的是指针之前的位置,这样划分内存时就出现不一致的问题,解决这种问题,虚拟机采用了循环CAS操作来保证内存的正确划分。
- 第二种也是为了解决第一种分配方式的不足而创建的方式,多线程分配内存时,虚拟机为每个线程分配了不同的空间,这样每个线程在分配内存时只是在自己的空间中操作,从而避免了上述问题,不需要同步。当然,当线程自己的空间用完了才需要需申请空间,这时候需要进行同步锁定。为每个线程分配的空间称为“本地线程分配缓冲(TLAB)”,是否启用TLAB需要通过 -XX:+/-UseTLAB参数来设定。
-
为对象的字段赋默认值;
分配完内存后,需要对对象的字段进行零值初始化(赋默认值),对象头除外。
零值初始化意思就是对对象的字段赋0值,或者null值,这也就解释了为什么这些字段在不需要进程初始化时候就能直接使用。
-
设置对象头;
对这个将要创建出来的对象,进行信息标记,包括是否为新生代/老年代,对象的哈希码,元数据信息,这些标记存放在对象头信息中。
-
执行实例的初始化方法lint
linit方法包含成员变量、构造代码块的初始化,按照声明的顺序执行。
-
执行构造方法。
执行对象的构造方法。至此,对象创建成功。
上述为无父类的对象创建过程。对于有父类的对象创建过程,还需满足如下条件:
- 先加载父类;再加载本类;
- 先执行父类的实例的初始化方法init(成员变量、构造代码块),父类的构造方法;执行本类的实例的初始化方法init(成员变量、构造代码块),本类的构造方法。
四、创建过程举例
1. 无父类的对象创建
对象创建源码
public class ClassA { private static int y = 1; private static String s = "1"; static { y=2; } private static int x = 1; static { s="2"; } static { x=2; } public ClassA() { x = x+1; y = y+1; s = "3"; } public static void main(String[] args) { ClassA classA = new ClassA(); } }
具体创建步骤
-
类未加载,先加载类;
-
链接阶段时,准备阶段,为静态变量赋默认值;
y = 0; s = null; x = 0;
-
初始化阶段时,为静态变量赋初始值(执行类的初始化方法clinit);
clinit方法包含静态变量、静态代码块,按照声明的顺序执行。
y = 1; s = "1"; y = 2; x = 1; s = "2"; x = 2;
-
-
为成员变量赋默认值;
aa = 0; bb = 0;
-
对象初始化,为成员变量赋初始值。
执行实例的初始化方法lint(成员变量、构造代码块)。
aa = 1; aa = 2; bb = 1; bb = 2;
-
执行构造方法。
aa = 3; bb = 3;
至此,对象创建完成。
各属性值情况如下:
2. 有父类的对象创建
对象创建源码
父类
@Data public class ClassParent { public static int p1 = 1; public int p2 = 1; { p2 = 2; } static { p1 = 2; } public ClassParent() { p2 = 3; } }
子类
@Data public class ClassChild extends ClassParent { private static int c1 = 1; private int c2 = 1; { c2 = 2; } static { c1 = 2; } public ClassChild() { super(); c2 = 3; } public static void main(String[] args) { ClassChild classA = new ClassChild(); } }
具体创建步骤
-
类未加载,先加载类;
先加载父类,再加载子类
- ClassParent类加载:链接阶段时,准备阶段,为静态变量赋默认值;
p1 = 0;
- ClassParent类加载:初始化阶段时,为静态变量赋初始值(执行类的初始化方法clinit);
p1 = 1; p1 = 2;
- ClassChild类加载:链接阶段时,准备阶段,为静态变量赋默认值;
c1 = 0;
- ClassChild类加载:初始化阶段时,为静态变量赋初始值(执行类的初始化方法clinit);
c1 = 1; c1 = 2;
- ClassParent类加载:链接阶段时,准备阶段,为静态变量赋默认值;
-
为成员变量赋默认值;
这里的父类之类执行顺序没去验证,个人认为是先父类后子类。
- ClassParent类:为成员变量赋默认值;
p2 = 0;
- ClassChild类:为成员变量赋默认值;
c2 = 0;
- ClassParent类:为成员变量赋默认值;
-
ClassParent类:对象初始化,为成员变量赋初始值。
p2 = 1; p2 = 2;
-
ClassParent类:执行构造方法。
p2 = 3;
-
ClassChild类:对象初始化,为成员变量赋初始值。
c2 = 1; c2 = 2;
-
ClassChild类:执行构造方法。
c2 = 3;
至此,对象创建完成。
各属性值情况如下:
-
JVM垃圾收集之——怎样判定一个对象是不是垃圾
2022-07-14 16:08:52现在,让我们来学习一下JVM中的重头戏,垃圾收集想要把一个对象当成垃圾回收掉,我们需要知道,不被需要和使用的对象才是垃圾,关键是怎么找到这些不被需要和使用的对象。这里我们有两个方法可以去判定一个对象...
学过了JVM的内存模型,了解了JVM将其管理的内存抽象为不同作用的内存工作区域,这个区域是连续,然后分为五个部分,各司其职。
链接: JVM内存模型——运行时数据区的特点和作用现在,让我们来学习一下JVM中的重头戏,垃圾收集
想要把一个对象当成垃圾回收掉,我们需要知道,不被需要和使用的对象才是垃圾,关键是怎么找到这些不被需要和使用的对象。
这里我们有两个方法可以去判定一个对象是不是垃圾:
1引用计数法
一个对象呢我给它做一个引用计数,假如一个对象目前有三个引用指向,那么给他记录一个引用数为3。接下来如果有一个引用消失了,变成二,再有一个引用消失变成一,最后当引用全部消失这个数变成零,当它变成零的时候,这对象成为了垃圾(Python 就是使用这样的方式)。
总结:
如果一个对象没有引用指向它的时候,或者说引用计数器里面的值为0的时候,表示该对象就是垃圾。
缺陷:当有循环引用的时候,导致无法回收掉本该是垃圾的对象。那Java是使用的这一种垃圾回收方法吗?
举个栗子:public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过 * */ private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; // 假设在这行发生GC,objA和objB是否能被回收? System.gc(); } public static void main(String[] args) { testGC(); } }
运行截图:
从上图可以看出,没有进行垃圾回收之前,内存占用11960K。进行垃圾回收之后,内存占用896K。说明对象确实被回收释放了。但如果按照引用计数算法,两个对象之间其实还存在着互相引用,即引用计数器的值为1,也就是说本来不应该被回收,所以这里使用的显然就不是引用计数算法。2可达性分析
Java是使用一种叫GC Root的算法,是什么意思呢?
从根上的引用去找对象,能够被根节点引用找到的对象都不是垃圾,不用回收,如果是从根节点引用找不到的对象都是垃圾。通过
GC Root
的对象,开始向下寻找,看某个对象是否可达能作为GC Root:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。
JVM标准里给出了以下几种可以当作GC Root的对象:
1.在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
2.在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
3.在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
4.在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
5.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
6.所有被同步锁(synchronized关键字)持有的对象。
7.反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。我们研究的一直都是怎么让一个对象去死,但是
3一个对象真的非死不可吗?
3.1对象的自我救赎
即使在可达性分析算法中不可达的对象,并不是”非死不可“,要真正宣告一个对象死亡,至少要经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
- 当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过。
虚拟机将这两种情况都视为”没有必要执行“。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做
F-Queue
的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。
这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。3.2finalize的作用
- finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。
- finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性
- 不建议用finalize方法完成“非内存资源”的清理工作。
3.3finalized的问题
- 一些与finalize相关的方法,由于一些致命的缺陷,已经被废弃了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法
- System.gc()与System.runFinalization()方法增加了finalize方法执行的机会,但不可盲目依赖它们
- Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行
- finalize方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完成finalize的执行
- 对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的
- finalize方法至多由GC执行一次(用户当然可以手动调用对象的finalize方法,但并不影响GC对finalize的行为)
由于Finalizer线程优先级相较于普通线程优先级要低,而根据Java的抢占式线程调度策略,优先级越低的线程,分配CPU的机会越少,因此当多线程创建重写finalize方法的对象时,Finalizer可能无法及时执行finalize方法,Finalizer线程回收对象的速度小于创建对象的速度时,会造成F-Queue越来越大,JVM内存无法及时释放,造成频繁的Young GC,然后是Full GC,乃至最终的OutOfMemoryError。
3.4finalize的执行过程(生命周期)
首先,大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
执行代码演示:public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, i am still alive :)"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); //对象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("once, i am dead :("); } // 下面这段代码与上面的完全相同,但是这次自救却失败了 SAVE_HOOK = null; System.gc(); // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("second, i am dead :("); } } }
从结果可以看出,SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。
-
《面向对象葵花宝典:思想、技巧与实践》样章
2015-12-22 10:29:03《面向对象葵花宝典:思想、技巧与实践》样章。 本书内容主要分为4部分: 面向对象基础:通过对面向对象的历史、发展,与面向过程的对比等相关背景知识的介绍,让读者对面向对象有一个更完整的认识;并深入地阐述了... -
JS的对象与内置对象详细介绍
2019-07-06 23:53:50JS的对象就是Object,还有独立于宿主环境的ECMAScript实现提供的内置对象:Array,String,Date,Math。。。 首先我们来学习一下JS的对象: Object: JS里有一句话叫万物皆对象(除了基本数据类型)。但是new String(’... -
如何实现面向对象
2022-05-05 15:48:09如何实现面向对象面向对象的定义面向对象的三大特征、面向对象的基础实现面向对象基本准则实例 面向对象的定义 对象是指具体的某一事物,在现实生活中能够看得见摸得着的事物。在面向对象程序设计中,对象指的是... -
js面向对象理解
2019-05-13 21:25:29面向对象的语言有一个标志,那就是类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。但是,ECMAScript 没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。 js(如果没有作特殊说明,本文... -
java对象的内存模型详解:内存模型及对象头的奥秘
2021-08-26 15:33:33在面试官的因势利导下,很多人对jvm的内存模型已经耳熟能详,但是对我们经常new 出来的对象,比如new Object(),你了解它的内存模型吗?本篇文章将带你走进对象内部,真正去了解这个你最熟悉,也最不熟悉的的对象。 ... -
Java类和对象 详解(一)
2016-10-06 20:48:02一、面向对象简述面向对象是一种现在最为流行的程序设计方法,几乎现在的所有应用都以面向对象为主了,最早的面向对象的概念实际上是由IBM提出的,在70年代的Smaltalk语言之中进行了应用,后来根据面向对象的设计... -
对象遍历(对象forEach遍历)
2021-03-04 14:27:33对象遍历(对象forEach遍历)对象遍历对象fon in 遍历对象keys 遍历对象values 遍历对象getOwnPropertyNames遍历使用Reflect.ownKeys(obj)遍历封装Object.forEach方法遍历 对象遍历 对象fon in 遍历 尝试遍历(获取... -
C++类和对象的使用之对象指针
2019-07-04 21:38:07类和对象的使用之对象指针 对象指针的一般概念 对象指针:指向类的成员的指针。在C++中,可以说明指向类的数据成员和成员函数的指针。 对象指针遵循一般变量指针的各种规则:类名 *对象指针名; 对象指针名*成员名;... -
对象里面的对象
2020-12-04 11:03:101.在Java中一定要认识到:一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。 2.任何对象变量的值都是对存储在另外一个地方的一个对象的引用。 3.new操作符的返回值也是一个引用。 ... -
【愚公系列】2022年01月 MinIO文件存储服务器-对象操作(Python版)
2022-01-28 23:51:56对象创建2.1 将对象的数据下载到文件中2.2 复制对象数据2.3 副本组合创建对象2.4 本地数据流上传到对象2.5 将文件中的数据上传到存储桶中的对象3.对象删除3.1 移除一个对象3.2 移除多个对象二、对象标签配置1.删除... -
JSP九大内置对象
2019-02-28 15:52:26JSP提供了由容器实现和管理的内置对象,也可以称之为隐含对象,由于JSP使用Java作为脚本语言,所以JSP将具有强大的对象处理能力,并且可以动态创建Web页面内容。但Java语法在使用一个对象前,需要先实例化这个对象,... -
java对象数组 创建对象数组,初始化对象数组
2019-07-30 16:34:15对象数组的概念: 如果一个数组中的元素是对象类型,则称该数组为对象数组。 当需要一个类的多个对象时,应该用该类的对象数组来表示,通过改变下标值就可以访问到不同的对象。 对象数组的定义和使用: 对象数组的... -
Java虚拟机中对象探秘--对象头创建、对象头、对象锁、synchoronized底层实现
2018-05-18 16:26:01今天看别人的博客,讲到面试相关的问题,其中有一个知识点是:synchronized关键字,Java对象头、Markword概念、synchronized底层实现,monitorenter和monitorexit指令,一脸蒙逼,虽然早期把《深入理解Java虚拟机》... -
C#中未将对象引用设置到对象的实例
2022-06-19 09:29:25今天要弄的这个东西是之前做这个项目遇到的一个Bug,未将对象引用设置到对象的实例, 导致这个问题出现呢是因为在截取字符串后三位的时候,局部变量strBig是空值。如下:因为此截图为现在项目现存的截图,与之前出现... -
浅谈JS包装对象
2022-01-19 10:38:32对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”(wrapper)。 所谓“包装对象”,指的是与数值、字符串、... -
遍历对象的方法
2022-03-09 09:34:59遍历对象的方法一、vue中v-for遍历对象二、for...in 遍历数组和对象都可以三、Object的方法四、Object.getOwnPropertyNames(obj)五、使用Reflect.ownKeys(obj)遍历 一、vue中v-for遍历对象 <el-form-item label=... -
js判断一个对象是否是空对象
2022-03-21 14:26:14js如何判断一个对象是否是空对象,本文给出三个方法。 二、采用for…in…进行遍历 最常见的思路就是使用for....in...遍历属性,为真则为非空对象,为假则为空对象。 <script> function isEmptyObj(obj) { ... -
面向对象的基础-抽象
2022-05-05 19:35:35面向对象的三大特征? 封装、继承、多态 面向对象的基础是什么? 抽象 为什么说抽象是面向对象的基础? 先来看两句话: 1、我们要注重多少人干事,而不是干多少事 2、我们要注重谁来干事,而不是怎么干事... -
JS 5种遍历对象的方式
2022-04-17 10:40:53for in 循环是最基础的遍历对象的方式,它还会得到对象原型链上的属性 // 创建一个对象并指定其原型,bar 为原型上的属性 const obj = Object.create({ bar: 'bar' }) // foo 为对象自身的属性 obj.foo = 'foo' ... -
java 判断一个对象是否为空对象
2020-03-30 22:29:29最近项目中遇到一个问题,在用户没填数据的时候,我们需要接收从前端传过来的对象为null,但是前端说他们一个一个判断特别麻烦,只能传个空对象过来,我第一个想法就是可以通过反射来判断对象是否为空。 第一版: ... -
js把两个对象合并成一个对象
2022-04-01 22:58:43Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象 合并对象 var o1 = { a: 1 }; var o2 = { b: 2 }; var o3 = { c: 3 }; var obj = Object.assign(o1, o2, o3... -
未将对象引用设置到对象的实例--可能出现的问题总结
2021-03-04 02:03:34一、网络上的一般说法1、ViewState 对象为Null。2、DateSet 空。3、sql语句或Datebase的原因导致DataReader空。4、声明字符串变量时未赋空值就应用变量。5、未用new初始化对象。6、Session对象为空。7、对控件赋文本... -
ts定义对象中对象类型
2021-08-19 15:43:04定义 type IPlanTagProps = { content: string; bg?: string; color?: string; ... [propName: string]: IPlanTagProps;... 'a': { content: 'aa', bg: '#E9EDFF', color: '#264AFF' }, 'b': { content: 'bb -
将js对象转化为json对象
2022-03-12 21:01:40<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> ... //编写 一个JavaScript对象, var user = { name: "asdas", -
vue 遍历对象
2022-01-05 20:41:35可以使用v-for 遍历对象,在M层定义普通对象, 举例:data 内容 data:{ obj:{ id:1, name:'名称', des:'des内容', content:'具体内容' } } 这个时候,用 v-for 遍历可以遍历对象。 <div v-for=... -
young GC 和 full GC分别是什么? 对象什么时候在栈上分配?对象进入老年代的机制是怎样的?
2021-07-20 16:17:43文章目录 前言 一、对象在栈上分配 二、对象在EDEN分配 三、 minor GC 和 full GC 四、 对象进入老年代 3.1大对象直接进入老年代 3.2长期存活的对象将进入老年代 3.3对象动态年龄判断 3.4老年代空间分配担保机制 五...