精华内容
下载资源
问答
  • 声明:此步骤告诉VM什么是变量静态足迹.例如:对象a;将只有在Object类中声明脚印可见,而Integer b;将在Integer类和所有继承父类中声明所有足迹,直到Object可见.这针对静态部分.> instanciation:此步骤...

    这根本不是多余的.使用变量有两个步骤:

    >声明:此步骤告诉VM什么是变量的静态足迹.例如:对象a;将只有在Object类中声明的脚印可见,而Integer b;将在Integer类和所有继承的父类中声明所有足迹,直到Object可见.这是针对静态部分的.

    > instanciation:此步骤告诉VM什么是变量的动态足迹.例如:List< String> c = new LinkedList< String>();,然后是c.put(“foo”);将使用LinkedList的put()方法实现,即使可见的是List :: put().有时,您将需要这种声明/实例,但需要覆盖以访问静态足迹不可见的非常具体的方法.例如,让我们考虑一个声明为public void method1(Object obj)的方法,并且您知道obj实例实际上是一个Integer,因此您将通过将对象转换为它来专门使用动态脚印:int value =((Integer) OBJ).intValue();

    现在,至于字符串a =“A”;部分.为简单起见,Java已经简化了“原始”类的写法.更具体地说,从Java 1.5开始,您可以:

    Integer n1 = 1;

    Integer n2 = new Integer(1); // same thing

    int n3 = n2;

    一切正常.但有什么区别?考虑这段代码:

    String a = new String("A");

    String b = new String("A");

    String c = "A";

    String d = "A";

    System.out.println("a:" + a.hashCode() + " = b:" + b.hashCode() + " == " + (a == b));

    System.out.println("b:" + b.hashCode() + " = c:" + c.hashCode() + " == " + (b == c));

    System.out.println("c:" + c.hashCode() + " = d:" + d.hashCode() + " == " + (c == d));

    将输出

    a:65 = b:65 == false

    b:65 = c:65 == false

    c:65 = d:65 == true

    为什么?因为JVM正在尝试尽可能多地重用内存,并且由于a和b正在创建String的新实例,因此它们不共享相同的内存空间.但是,c和d使用常量字符串值(这是编译器优化),因此指向完全相同的String对象.

    展开全文
  • Python学习手册笔记(四)介绍Python对象类型在Python中,数据以对象的形式出现——无论Python提供的内置对象,还是使用Python或是像C扩展库这样的扩展语言工具创建的对象。对象无非内存中的一部分,包含数值和...

    Python学习手册笔记(四)介绍Python对象类型

    在Python中,数据以对象的形式出现——无论是Python提供的内置对象,还是使用Python或是像C扩展库这样的扩展语言工具创建的对象。对象无非是内存中的一部分,包含数值和相关操作的集合。

    Python程序可以分解成模块、语句、表达式以及对象,如下所示

    1.程序由模块构成

    2.模块包含语句

    3.语句包含表达式

    4.表达式建立并处理对象

    1为什么使用内置类型

    使用底层语言C或C++,要花很大一部分工作集中于用对象(或者叫数据结构)去表现应用领域的组件。需要部署内存结构、管理内存分配、实现搜索和读取例程等工作。这些工作听起来乏味,容易出错,而且往往背离程序的真正目标。

    Python提供了强大的对象类型作为语言的组成部分,事实上,除非有内置类型无法提供的特殊对象要处理,最好总是使用内置对象而不是使用自己的实现。下面是原因

    l内置对象使程序更容易编写。内置对象往往能够表现问题领域的所有结构,仅适用内置对象就能够完成很多工作。

    l内置对象是扩展的组件。人工实现的对象往往建立在像列表和字典这样的内置类型的基础上。

    l内置对象往往比定制的数据结构更有效率。在速度方面,Python的内置对象类型优化了用C实现数据结构算法。

    l内置对象是语言的标准的一部分。Python的内置工具是标准的,它们一般都是一致的,另一方面,独创的框架则在不同的环境都有所不同。

    2Python的核心数据类型

    内置对象

    对象类型

    例子

    常量/创建

    数字

    1234,3.1415,3+4j,Decimal,Fraction

    字符串

    ‘spam’, “guido’s”,

    b’a\xolc’

    列表

    [1, [2,

    ‘three’],4]

    字典

    {‘food’:’spam’,

    ‘taste’:’yum’}

    元组

    (1, ‘spam’, 4,

    ‘U’)

    文件

    myfile = open(‘eggs’,

    ‘r’)

    集合

    set(‘abc’), {‘a’, ‘b’,

    ‘c’}

    其他类型

    type、None、布尔型

    编程单元类型

    函数、模块、类

    与实现相关的类型

    编译的代码堆栈跟踪

    表并非完整,因为在Python程序中处理的每样东西都是一种对象。表中的对象类型称作为核心数据类型,因为它们是在Python语言内部高效创建的。也就是说,有一些特定的语法可以生成它们。

    同样重要的是,一旦创建了一个对象,它就和操作集合绑定了——只可以对字符串进行字符串相关的操作,对列表进行列表相关的操作。Python是动态类型的(它自动跟踪你的类型而不是要求声明代码),但是它也是强类型语言(你只能对一个对象进行适合该类型有效的操作)。

    3数字

    常规类型:整数、浮点数,以及更加少见的类型:有虚部的复数、固定精度的十进制数、带分子和分母的有理数以及集合等。

    Python中的数字支持一般的数学运算,例如加法(+)、乘法(*)、乘方(**)等。

    注意计算2 **

    100的结果:当需要时,Python

    3.0的整数类型会自动提供额外的精度,以用于较大的数值。

    一旦接触浮点数,可能会遇到一些奇怪的事情(无缘无故多出一些小数点,但是在新的版本可能会自动解决),也可以使用print来解决

    >>>3.1415

    * 2 # repr: as code

    6.283000000000004

    >>>print(3.1415

    * 2) # str: suer-friendly

    6.283

    第一种形式看做是对象的代码形式repr,第二种是它的用户友好形式str。当使用类时,这两者的区别将会体现出来。

    除表达式外,和Python一起分发的还有一些常用的数学模块,模块只不过是我们导入以供使用的一些额外工具包。

    >>>import

    math

    >>>math.pi

    3.1415926535897931

    >>>math.sqrt(85)

    9.2195444572928871

    Math模块包括更高级的数学工具,如函数,而random模块可以作为随机数字的生成器和随机选择器

    >>>import

    random

    >>>random.random()

    0.59268735266273953

    >>>random.choice([1,

    2, 3, 4])

    1

    Python还包括一些较为少见的数字对象,例如复数、固定精度十进制数、有理数、集合和布尔值,第三方开源扩展领域甚至包含了更多(矩阵和向量)。

    4字符串

    就像任意字符的集合一样,字符床是用来记录文本信息的。它们是在Python中作为序列(一个包含其他对象的有序集合)提到的第一个例子。序列中的元素包含了一个从左到右的顺序——序列中的元素根据它们的相对位置进行存储和读取。从严格意义上来说,字符串是单个字符的字符串的序列,其他类型的序列还包括列表和元组

    4.1序列的操作

    可以通过内置的len函数验证字符串长度,并通过索引操作得到各个元素

    >>>

    S= ‘Spam’

    >>>len(S)

    # Length

    4

    >>>S[0]

    # The first item in S. indexing by zero-based

    position

    ‘S’

    在Python中,索引是按照最前面的偏移量进行编码的,即从0开始,第一项索引为0,第二项索引为1,以此类推。

    Python变量不需要提前声明,当给一个变量赋值时就创建了它,可能赋的是任何类型的对象,并且当变量出现在一个表达式时,就会用其值替换它。在使用变量的值之前必须对其赋值。我们需要把一个对象赋给一个变量以便保存它供以后使用。

    在Python,我们能够反向索引,从最后一个开始。

    >>>S[-1]

    # The last item from the end in S

    ‘m’

    >>>S[-2]

    # The second to last item from the

    end

    ‘a’

    一般来说,负的索引号会简单地与字符串的长度相加,因此,以下两个操作是等价的:

    >>>S[-1]

    >>>S[len(S)-1]

    我们能够在方括号中使用任意表达式,而不仅仅是使用数字常量——只要Python需要一个值,我们可以使用一个常量、一个变量或者任意表达式。Python的语法在这方面是完全通用的。

    除了简单地从位置进行索引,序列也支持一种所谓的分配(slice)操作,这是一种一步就能提取整个分片(slice)的方法。例如:

    >>>

    S

    ‘Spam’

    >>>S[1:3]

    ‘pa’

    一般形式为X[I:J],表示“去除在X中从偏移量为I,直到但不包括偏移量为J的内容”

    在一个分片中,左边界默认为0,并且右边界默认为分片序列的长度。这引入了一些常用法的变体:

    >>>S[1:]

    ‘pam’

    >>>

    S

    ‘Spam’

    >>>S[0:3]

    ‘Spa’

    >>>S[:-1]

    ‘Spa’

    >>>S[:]

    ‘Spam’

    最后,作为一个序列,字符串也支持使用加号进行合并(将两个字符串合成为一个新的字符串),或者重复(通过再重复一次创建一个新的字符串):

    >>>

    S

    ‘Spam’

    >>>

    S+ ‘xyz’ # Concatenation

    ‘Spamxyz’

    >>>

    S# S is unchanged

    ‘Spam’

    >>>

    S* 8 # Repetition

    ‘SpamSpamSpamSpamSpamSpamSpamSpam’

    注意加号(+)对于不同的对象有不同的意义:对于数字为加法,对于字符串为合并。这是Python的一般特性:多态。简而言之,一个操作的意义取决于被操作的对象。这种多态的特性给Python代码带来了很大的简洁性和灵活性。由于类型并不受约束,Python编写的操作通常可以自动适用于不同类型的对象,只要它们支持一种兼容的借口(如同这里的+操作)。

    4.2不可变性

    每个字符串都被定义为生成的字符串作为结果,因为字符串在Python中具有不可变性——在创建后不能就地改变。但是你可以通过建立一个新的字符串并以同一个变量名对其进行赋值,因为Python在运行过程中会清理旧的对象

    >>>

    S

    ‘Spam’

    >>>S[0]

    = ‘z’ # Immutable objects cannot be

    changed

    …error

    textomitted…

    TypeError: ‘str’object does not support

    item assignment

    >>>

    S= ‘z’ + S[1:] # But we can run

    expre_ssions(表达式)

    to make new objects

    >>>

    S

    ‘zpam’

    在Python中的每一个对象都可以分为不可变性和可变性。在核心类型中,数字、字符串和元组是不可变的;列表和字典可变。在其他地方,这种不可变性可以用来保证在程序中保持一个对象固定不变。

    4.3类型特定的方法

    字符串还有独有的一些操作作为方法存在(对象的函数,将会通过一个调用表达式触发)。

    例如,字符串的find方法是一个基本的子字符串查找的操作(它将返回一个传入子字符串的偏移量,或者没有找到的情况下返回-1),而字符串的replace方法将会对全局进行搜索和替换。

    >>>S.find(‘pa’)

    # Find the offset of a substring

    1

    >>>

    S

    ‘Spam’

    >>>S.replace(‘pa’,

    ‘XYZ’) # Replace occurrences of a substring with

    another

    ‘SXYZm’

    >>>

    S

    ‘Spam’

    尽管这些字符串方法的命名有改变的含义,但在这里我们都不会改变原始的字符串,而是会创建一个新的字符串作为结果——因为字符串具有不可变性。字符串方法将是Python中文版处理的头号工具。其他的方法还能够实现通过分隔符将字符串拆分成子字符串,大小写变化,测试字符串的内容,去掉字符串后的空格字符。

    >>>line

    = ‘aaa,bbb,ccccc,dd’

    >>>line.split(‘,’)

    # Split on a delimiter into a list of

    substrings

    [‘aaa’, ‘bbb’,‘ccccc’,

    ‘dd’]

    >>>

    S= ‘spam’

    >>>S.upper()

    # Upper-and lowercase conversions

    ‘SPAM’

    >>>S.isalpha()

    # Content tests: isalpha, isdigit,

    etc.

    True

    >>>line

    = ‘aaa,bbb,ccccc,dd\n’

    >>>line

    = line.rstrip() # Remove whitespace characters on the right

    side

    >>>line

    ‘aaa,bbb,ccccc,dd’

    字符串还支持一个叫做格式化的高级替代操作,可以以一个表达式的形式(最初的)和一个字符串方法调用(Python2.6和Python3.0新引入的)形式使用:

    >>>

    ‘%s,eggs, and %s’ %(‘spam’, ‘SPAM!’) # Formatting

    expre_ssion(all)

    ‘spam, eggs,and

    SPAM!’

    >>>

    ‘{0},eggs, and {1}’ %(‘spam’, ‘SPAM!’) .format(‘spam’, ‘SPAM!’) #

    Formattingmethod(2.6, 3.0)

    ‘spam, eggs,and

    SPAM!’

    注意:尽管序列操作是通用的,但方法不通用(虽然某些类型共享某些方法名,字符串的方法只能用于字符串)。一条简明的法则是这样的:可作用于多种类型的通用型操作都是以内置函数或表达式的形式出现的[例如,len(X),X[0]],但是类型特定的操作是以方法调用的形式出现的[例如,aString.Upper()]。

    4.4寻求帮助

    对于更多细节,可以调用内置的dir函数,将会返回一个列表,其中包含了对象的所有属性。由于方法是函数属性,它们也会在这个列表出现。假设S是一个字符串

    >>>dir(S)

    一般来说,以双下划线开头并结为的变量名是用来表示Python实现细节的命名模式。而列表中没有下划线的属性是字符串对象能够调用的方法。

    dir函数简单地给出了方法的名称,要查询它们是做什么的,可以将其传递给help函数。

    >>>help(S.replace)

    就像PyDoc一样(一个从对象中提取文档的工具),help是一个随Python一起分发的面向系统代码的结构。PyDoc也能够将其结果生成HTML格式。

    也能够对整个字符串提交帮助查询[如help(S)],但一般最好查询一个特定的方法。

    想获得更多细节,可以参考Python的标准库参考手册或商业出版的参考书,但是dir和help是Python文档的首要选择。

    4.5编写字符串的其他方法

    Python还提供了各种编写字符串的方法,例如,反斜线转义序列表示特殊的字符。

    >>>

    S= ‘A\nB\tC’ # \n is end-of-line, \t is

    tab

    >>>len(S)

    # Each stands for just one

    character

    5

    >>>ord(‘\n’)

    # \n is a byte with the binary value 10 in

    ASCII

    10

    >>>

    S= ‘A\0B\0C’ # \0, a binary zero byte, does not terminate

    string

    >>>len(S)

    5

    Python运行字符串包括在单引号和双引号中(代表意义相同)。它也运行三个引号(单引号或双引号)中包括多行字符串常量。当采用这种形式时,所有的航都合并在一起,并在每一行的末尾增加换位符。这是一个微妙的语法上的便捷方式,但是在Python脚本中嵌入HTML或XML之类的内容时,这是十分方便的。

    >>>msg

    = “””aaaaaaaaaaa

    bbb’’’bbbbbbbbbbbb””bbbbb’bbbb

    cccccccccccccccc”””

    >>>msg

    ‘\naaaaaaaaaaa\nbbb\’\’\’bbbbbbbbbbbb””bbbbb\’bbbb\ncccccccccccccccc’

    Python也支持原始(raw)字符串常量,即去掉反斜线转义机制(这样的字符串常量是以字母“r”开头的)。Python还支持Unicode字符串形式从而支持国际化。在Python3.0中,基本的str字符串类型也处理Unicode(这是有意义的,因为ASCII文本是一种简单的Unicode),并且用bytes类型表示原始字符串;在Python2.6中,Unicode是一种单独的类型,str处理8位字符串和二进制数据。在Python3.0中,文件也改变为返回和接受str,从而处理二进制数据的文本和字节。

    4.6模式匹配

    字符串对象的方法能够支持基于模式的文本处理。文本的模式匹配是本书范围以外的一个高级工具,我们需要导入一个名为re的模块。这个模块包含了类似搜索、分割和替换等调用,但是因为使用模式去定义字符串,可以更通用一些:

    >>>import

    re

    >>>match

    = re.match(‘Hello[ \t]*(.*)world’,

    ‘HelloPython

    world’)

    >>>match.group(1)

    ‘Python’

    这个例子的目的是搜索子字符串,这个子字符串以“Hello”开始,后面跟着零个或几个制表符或空格,接着又任意字符并将其保存至匹配的group中,最后以“world.”结尾。如果找到了这样的子字符串,与模式中括号包含的部分匹配的子字符串的对应部分保存为组。例如,下面的模式取出了三个被斜线所分割的组。

    >>>match

    = re.match(‘/(.*)/(.*)/(.*)’,

    ‘/usr/home/lumberjack’)

    >>>match.groups()

    (‘usr’,

    ‘home’,‘lumberjack’)

    模式匹配本身是一个相当高级的文本处理工具,但是在Python中还支持更高级的语言处理工具,包括自然语言处理等。

    5列表

    列表是Python提供的最通用的序列。列表是一个任意类型对象的位置相关的有序集合,它没有固定的大小。不像字符串,其大小是可变的,通过对偏移量进行赋值以及其他各种列表的方法进行调用,确实能够修改列表的大小

    5.1序列操作

    列表是序列的一种,列表支持所有我们对字符串所讨论过的序列操作。唯一的区别就是其结果往往是列表而不是字符串。

    >>>

    L= [123, ‘spam’, 1.23] # A list of three different-type

    objects

    >>>len(L)

    # Number of items in the list

    3

    能对列表进行索引、切片等操作

    >>>L[0]

    # Indexing by position

    123

    >>>L[:-1]

    # Slicing a list returns a new list

    [123,

    ‘spam’]

    >>>

    L+ [4, 5, 6] # Concatenation makes a new list

    too

    [123, ‘spam’,1,23, 4,

    5,6]

    >>>

    L# We’re not changing the original

    list

    [123, ‘spam’,

    1.23]

    5.2类型特定的操作

    列表没有固定大小,能够按照需要增加或减小列表大小,来响应其特定的操作:

    >>>L.append(‘NI’)

    # Growing: add object at end of

    list

    >>>

    L

    [123, ‘spam’,1.23,

    ‘NI’]

    >>>L.pop(2)

    # Shrinking: delete an item in the

    middle

    1.23

    >>>

    L# “del L[2]” deletes from a list

    too

    [123, ‘spam’,

    ‘NI’]

    列表的append方法扩充了列表的大小并在列表的尾部插入一项;pop方法(或者等效的del语句)移除给定偏移量的一项。其他列表方法可以在任意位置插入(insert)元素,按照值移除(remove)元素等。因为列表是可变的,大多数列表方法都会直接改变列表对象,而不是创建一个新的列表:

    >>>

    M= [‘bb’, ‘aa’, ‘cc’]

    >>>M.sort()

    >>>

    M

    [‘aa’, ‘bb’,

    ‘cc’]

    >>>M.reverse()

    >>>

    M

    [‘cc’, ‘bb’,

    ‘aa’]

    这里的列表sort方法,默认按照升序对列表进行排序,而reverse对列表进行翻转,都直接改变了列表。

    5.3边界检查

    尽管列表没有固定的大小,Python仍然不允许引用不存在的元素。超出列表末尾之外的索引总是会导致错误,对列表末尾范围之外赋值也是如此。

    这是有意为之,由于去给一个列表边界外的元素赋值,往往会得到一个错误(在C语言中比较糟糕,因为它不会像Python这样进行错误检查)。在Python中,并不是默默地增大列表响应,而是会提示错误。为了让列表增大,我们可以调用append这样的列表方法。

    5.4嵌套

    Python核心数据类型的一个优秀特型是它们支持任意的嵌套。能够以任意的组合对其进行嵌套,并可以多个层次进行嵌套(例如让一个列表包含字典,并在字典中包含另一个列表等)。这种特性的一个直接的应用就是实现矩阵,或者Python中的“多维数组”。一个嵌套列表的列表能够完成这个基本的操作:

    >>>

    M= [[1,2,3],[4,5,6],[7,8,9]] # A 3*3 matrix, as nested

    lists

    >>>

    M

    [[1,2,3],[4,5,6],[7,8,9]]

    其效果是表现了一个3*3的数字矩阵。这样的结构可以通过多种方法获取元素。

    >>>M[1]

    # Get row 2

    [4,5,6]

    >>>M[1][2]

    # Get row 2, then get item 3 within the

    row

    6

    这里的第一个操作读取了整个第二行,第二个操作读取了那行的第三个元素。串联起索引操作可以逐层深入地获取嵌套的对象结构。

    5.5列表解析

    Python还包括一个更高级的操作,称作列表解析表达式(list

    comprehension

    expre_ssion),从而提供了一种处理矩阵这样结构的强大工具。例如,我们需要从列举的矩阵中提取出第二列。因为矩阵是按照行进行存储的,索引通过简单的索引即可获取行,使用列表解析可以简单地获得列。

    >>>col2

    = [row[1] for row in M] # Collect the items in column

    2

    >>>col2

    [2, 5,

    8]

    列表解析来源自己和的概念。它是一种通过对序列中的每一项运行一个表达式来创建一个新列表的方法,每次一个,从左到右。列表解析是编写在方括号中的(提醒你在创建列表这个事实),并且由使用了同一个变量名(这里是row)表达式和循环结构组成。之前的这个列表解析式表达的基本就是它字面所讲:“把矩阵M的每个row中的row[1],放在一个新的列表中”。其结果就是一个包含了矩阵的第二列的新列表。

    实际应用中的列表解析可以更复杂:

    >>>[row[1]

    + 1 for row in M] # add 1 to each item in column

    2

    [3, 6,

    9]

    >>>[row[1]

    for row in M if row[1] % 2 == 0] # Filter out odd

    item

    [2, 8]

    第一个操作把搜集到的每个元素加一,第二个使用了一个if条件语句,通过使用%求余表达式(取余数)过滤了结果中的奇数。列表解析创建了新的列表作为结果,但是能够在任何可迭代的对象上进行迭代。例如,可以用列表解析去步进坐标的一个硬编码列表和一个字符串。

    >>>diag

    = [M[i][i] for i in [0,1,2]] # Collect a diagonal from

    matrix

    >>>diag

    [1,5,9]

    >>>doubles

    = [c * 2 for c in ‘spam’] # Repeat characters in a

    string

    >>>doubles

    [‘ss’, ‘pp’,

    ‘aa’,‘mm’]

    列表解析以及相关的内容函数map和filter比较复杂。

    括号中的解析语法也可以用来创建产生所需结果的生成器(例如内置的sum函数,按一种顺序汇总各项):

    >>>

    G= (sum(row) for row in M) # Create a generator of row

    sums

    >>>next(G)

    6

    >>>next(G)

    # Run the iteration protocol

    15

    内置函数map可以做类似的事情,产生对各项运行一个函数的结果。在Python3.0中将其包装到列表中,会使其返回所有值:

    >>>list(map(sum,

    M)) # Map sum over items in M

    [6, 15,

    24]

    在Python3.0中,解析语法也可以用来创建集合和字典

    >>>{sum(row)

    for row in M} # Map sum over items in

    M

    {24,6,15}

    >>>

    {i: sum(M[i]) for i in range(3)} # Creates key/value table of row

    sums

    {0 : 6, 1:15,2 :

    24}

    实际上,在Python3.0中,列表、集合和字典都可以用解析来创建:

    >>>[ord(x)

    for x in ‘spaam’] # List of character

    ordinals

    [115, 112, 97,97,

    109]

    >>>{ord(x)

    for x in ‘spaam’} # Sets remove

    duplicates

    {112, 987

    115,109}

    >>>{x:

    ord(x) for x in ‘spaam’} # Dictionary keys are

    unique

    {‘a’: 97, ‘p’:112,‘s’:115,

    ‘m’:109}

    6字典

    字典不是序列,而是一种映射(mapping)。映射是一个其他对象的集合,但是它们是通过键而不是相对位置存储的。实际上,映射并没有任何可靠的从左到右的顺序。它们简单地将将映射到值。字典是Python核心对象集合中的唯一的一种映射类型,也具有可变性——可以就地改变,并可以随需求增大或减小。

    6.1映射操作

    作为常量编写时,字典编写在大括号中,并包含一系列的“键:值”对。在我们需要将键与一系列值相关联(例如,为了表述某物的某属性)的时候,字典是很有用的。

    >>>

    D= {‘food’:’Spam’, ‘quantity’:4, ‘color’:

    ‘pink’}

    我们可以通过键对这个字典进行索引来读取或改变键所关联的值。字典的索引操作使用的是和序列相同的语法,但是在方括号中的元素是键,而不是相对位置。

    >>

    D[‘food’]# Fetch value of key

    ‘food’

    ‘Spam’

    >>>D[‘quantity’]

    += 1 # Add 1 to ‘quantity’ value

    >>>

    D

    {‘food’:’Spam’,‘color’: ‘pink’

    ,‘quantity’:5}

    也可以开始一个空的字典,然后每次以一个键来填写它。与列表中禁止边界外的赋值不同,对一个新的字典的键赋值会创建该键:

    >>>

    D= {}

    >>>D[‘name’]

    = ‘Bob’ # Create keys by assignment

    >>>D[‘job’]

    = ‘dev’

    >>>D[‘age’]

    = 40

    >>>

    D

    {‘age’:40,

    ‘job’:’dev’,’name’:’Bob’}

    >>>print(D[‘name’])

    Bob

    在这里,我们实际上是使用字典上的键,如描述某人的记录中的名字字段。在另一个应用中,字典也可以用来执行搜索。通过键索引一个字典往往是Python中编写搜索的最快方法。

    6.2重访嵌套

    下面这个字典,一次将所有内容编写进一个常量,将可以记录更多的结构化信息。

    >>>rec

    = {‘name’:{‘first’:’Bob’, ‘last’:’Smith’}, ‘job’:[‘dev’,’mgr’],

    ‘age’:40.5}

    在这里,在顶层使用了三个键的字典,但是值的情况更为复杂:一个嵌套的字典作为name的值,支持了多个部分,并用一个嵌套的列表作为job的值从而支持多个角色和未来的扩展。能够获取这个结构的组件,但是这次索引的是字典的值,而不是偏移量。

    >>>rec[‘name’]

    # ‘name’ is a nested dictionary

    {‘first’:’Bob’,‘last’:’Smith’}

    >>>rec[‘name’][‘last’]

    # Index the nested dictionary

    ‘Smith’

    >>>rec[‘job’]

    # ‘job’ is a nested list

    [‘dev’,

    ‘mgr’]

    >>>rec[‘job’][-1]

    # Index the nested list

    ‘mgr’

    >>>rec[‘job’].append(‘janitor’)

    # Expand Bob’s job description

    in-place

    >>>rec

    {‘age’:40.5,

    ‘job’:[‘dev’,’mgr’,’janitor’],’name’:{‘last’:’Smith’,’first’:’Bob’}}

    注意最后一个操作是如何扩展嵌入job列表的。因为job列表是字典所包含的一部分独立的内存,它可以自由地增加或减少。

    介绍这个例子是为了说明Python核心数据类型的灵活性,嵌套运行直接并轻松地建立复杂的信息结构。使用C这样的底层语言建立一个类似的结构,将会很枯燥并使用更多的代码——不得不事先安排并声明结构和数组,填写值,将每一个都连接起来等。在Python这些都是自动完成的——运行表达式创建了整个的嵌套对象结构,这也是Python这样的脚步语言的优点之一。

    同样,在底层语言中,当我们不再需要该对象时,必须小心地去释放所有对象空间。在Python中,当最后一次引用对象后(例如重新赋值),这个对象所占用的内存空间将会自动清理掉。

    从技术来说,Python具有一种叫做垃圾收集的特性,在程序运行时可以清理不再使用的内存,并将你从必须管理代码中这样的细节中解放出来。在Python中,一旦一个对象的最后一次引用被移除,空间将会立即回收。

    6.3键的排序:for循环

    字典不是序列,不包含任何可靠的从左到右的顺序,因此对字典的打印也许与输入时树勋不同。

    >>>

    D= {‘a’:1, ‘b’:2, ‘c’:3}

    >>>

    D

    {‘a’:1,

    ‘c’:3,‘b’:2}

    如果需要强调某种顺序,常用的解决办法是通过字典的keys方法收集一个键的列表,使用列表的sort方法进行排序,然后使用Python的for循环逐个显示结果。

    >>>Ks

    = list(D.keys()) # Unordered keys

    list

    >>>Ks

    [‘a’, ‘c’,

    ‘b’]

    >>>Ks.sort()

    # Sorted keys list

    >>>Ks

    [‘a’,’b’,’c’]

    >>>for

    key in Ks # Iterate though sorted

    keys

    print

    (key, ‘=>’, D[key]) # <== pressEnter

    twice here

    a =>

    1

    b =>

    2

    c =>

    3

    这是一个有三个步骤的过程,不过在最新版本的Python中,可以通过最新的sorted内置函数一步完成。sorted调用返回结果并对各种对象类型进行排序。

    >>>for

    key in sorted(D):

    print

    (key, ‘=>’, D[key])

    a =>

    1

    b =>

    2

    c =>

    3

    for循环和while循环,是在脚本中编写重复性任务语句的主要方法。事实上,尽管这样,for循环就像它的亲戚列表解析一样是一个序列操作。它可以使用任意一个序列对象,并且就像列表解析一样,甚至可以在一些不是序列的对象中使用。

    >>>for

    c in ‘spam’:

    print(c.upper())

    S

    P

    A

    M

    Python的while虚幻更为常见,它不仅限于遍历序列:

    >>>

    x= 4

    >>>while

    x > 0:

    print(‘spam!’

    * x)

    x

    -= 1

    spam! spam!

    spam!spam!

    spam!

    spam!spam!

    spam!

    spam!

    spam!

    6.4迭代和优化

    for循环和列表解析式一样,都是真正的通用迭代工具。事实上,它们都能够工作于遵守迭代协议(Python中无处不在的一个概念,表示在内存中物理存储的序列,或一个在迭代操作情况下每次产生一个元素的对象)的任意对象。如果一个对象在响应next知其那先用一个对象对iter内置函数做出响应,那么它属于后一种情况。我们在前面看到的生成器解析表达式就是这样的一个对象。

    本书随后将会详细介绍迭代协议。只要记住,从左到右地扫描一个对象的每个Python工具都是用迭代协议。这就是为什么sorted调用直接作用于字典,因为字典是可迭代的对象,可以用一个next返回后续的键。

    所以可以进行如下操作

    >>>squares

    = [x ** 2 for x in [1,2,3,4,5]]

    >>>squares

    [1,4,9,16,25]

    尽管这样,列表解析和相关的函数编程工具,如map和filter,通常运行得比for循环快(也许快了两倍):这是对有大数据集合的程序有重大影响的特性之一。

    6.5不存在的键:if测试

    获取一个不存在的键值仍然是一个错误。如果在不知道当前存在什么键的情况下,可以先进行测试。

    in关系表达式允许我们查询字典中一个键是否存在,并可以通过使用Python中的if语句对结果进行分支处理。

    >>>

    ‘f’in D

    False

    >>>if

    not ‘f’ in D:

    print(‘missing’)

    missing

    这里有其他的方法来创建字典并避免获取不存在的字典键:get方法(带有一个默认值的条件索引)、Python

    2.X的has_key方法(3.0中不可用)、try语句(一个捕获异常并从异常中恢复的工具),以及if/else表达式:

    >>>value

    = D.get(‘x’, 0) # Index but with a

    default

    >>>value

    0

    >>>value

    = D[‘x’] if ‘x’ in D else 0

    >>>value

    0

    7元组

    元组对象(tuple)基本就像一个不可改变的列表。元组类似列表,是序列,但是具有不可变性,和字符串来类似。从语法来讲,它们编写在圆括号中而不是方括号中,它们支持任意类型、任意嵌套以及常见的序列操作:

    >>>

    T= (1,2,3,4) # A 4-item tuple

    >>>len(T)

    # Length

    4

    >>>

    T+ (5,6) # Concatenation

    (1,2,3,4,5,6)

    >>>T[0]

    # Indexing, slicing, and more

    在Python3.0中,元组还有两个专有的可调用方法,但它的专有方法不像列表所拥有的那么多:

    >>>

    T.index(4)# Tuple methods: 4 appears at offset

    3

    3

    >>>T.count(4)

    # 4 appears once

    1

    元组一旦创建后不可改变,是不可不的序列

    >>>T[0]

    = 2 # Tuples are immutable

    …error

    textomitted…

    TypeError: ‘tuple’object does not

    support item assignment

    与列表和字典一样,元组支持混合的类型和嵌套,但是不能增长或缩短,因为不可变:

    >>>

    T= (‘spam’,3.0,[11,22,33])

    >>>T[1]

    3.0

    >>>T[2][1]

    22

    >>>T.append(4)

    AttributeError:’tuple’object has no

    attribute ‘append’

    7.1为什么要用元组

    元组使用的关键在于其不可变性,提供了一种完整性的约束,这对于我们所编写的更大型的程序来说是方便的。

    8文件

    文件对象是Python代码对电脑上外部文件的主要接口。虽然文件是核心类型,但是比较特殊:没有特地的常量语法创建文件。要创建一个文件对象,需要调用内置的open函数以字符串的形式传递给它一个外部的文件名以及一个处理模式的字符串。例如,创建一个文本输出文件,可以传递其文件名以及‘w’处理模式字符串以写数据:

    >>>

    f= open(‘data.txt’,’w’) # Make a new file in output

    mode

    >>>f.write(‘Hello\n’)

    # Write strings of bytes to it

    6

    >>>f.write(‘world\n’)

    # Returns number of bytes written in Python

    3.0

    6

    >>>f.close()

    # Close to flush output buffers to

    disk

    这样就在当前文件夹下创建了一个文件,并向它吸入文本(文件名可以是完整路径)。为了读出缩写的内容,重新以’r’处理模式打开文件,读取输入(如果在调用时忽略,则默认该模式)。之后将文件中的内容读至一个字符串,并显示它。对脚本而已,文件的内容总是字符串,无论文件包含的数据是什么类型。

    >>>f=

    open(‘data.txt’) # ‘r’ is the default processing

    mode

    >>>text

    = f.read()

    # Read entirefile into a

    string

    >>>text

    ‘Hello\nworld\n’

    >>>print(text)

    # print interprets control

    characters

    Hello

    world

    >>>text.split()

    # File content is always a string

    [‘Hello’,

    ‘world’]

    这里对其他的文件对象方法支持的特性不进行讨论。例如,文件对象提供了多种读和写的方法(read可以接受一个字节大小的选项,readline每次读一行等),以及其他的工具(seek移动到一个新的文件位置)。从本书后面会知道,如今读取一个文件的最佳方式是根本不读它,文件提供了一个迭代器(iterator),它在for循环或其他环境中自动地一行一行地读取。

    Python

    3.0中的文件在文本和二进制数据之间划出了一条清晰的界限。文本文件把内容显示为字符串,并且自动执行Unicode编码和解码;而二进制文件把内容显示为一个特定的字节字符串类型,并且允许你不修改地访问文件内容:

    >>>data

    = open(‘data.bin’, ‘rb’).read() # Open binary

    file

    >>>data

    b’\x00\x00\x00\x07spam\x00\x08’

    >>>data[4:8]

    b’spam’

    如果只处理ASCII文本,通常不需要关注,但如果你处理国际化的应用程序或者面向字节的数据,Python

    3.0的字符串和文件十分有用。

    8.1其他文件类工具

    open函数能够实现在Python中编写的绝大多数文件处理。尽管如此,对于更高级的任务,Python还有额外的类文件工具:管道、先进先出队列(FIFO)、套接字、通过键访问文件、对象持久、基于描述符的文件、关系数据库和面向对象数据库接口等。例如,描述符文件(descriptor

    file)支持文件锁定和其他的底层工具,而套接字提供网络和进程间通信的接口。

    9其他核心类型

    例如,集合是最近增加到这门语言中的类型,即非映射,也非序列,它们是唯一的不可变的对象的无序结合。集合可以通过调用内置set函数创建,或使用Python3.0中新的集合常量和表达式创建,并且它支持一般的数学集合操作。

    >>>

    X= set(‘spam’) # Make a set out of a sequence in 2.6 and

    3.0

    >>>

    Y= {‘h’, ‘a’, ‘m’} # Make a set with new 3.0 set

    literals

    ({‘a’,’p’,’s’,’m’},{‘a’,’h’,’m’}

    >>>

    X& Y # Intersection

    {‘a’,’m’}

    >>>

    X| Y # Union

    {‘a’,’p’,’s’,’h’,’m’}

    >>>

    X– Y # Difference

    {‘p’,

    ‘s’}

    >>>{x

    ** 2 for x in [1,2,3,4]}

    {16,1,4,9}

    此外,Python最近添加了一些新的数值类型:十进制数(固定精度浮点数)和分数。它们都用来解决浮点数学的局限性和内在的不精确性:

    >>>1/3

    # Floating-point( use .0 in Python

    2.6)

    0.3333333333331

    >>>(2/3)

    + (1/2)

    1.1666666666665

    >>>import

    decimal # Decimals:fixed precision

    >>>

    d= decimal.Decimal(‘3.141’)

    >>>

    d+ 1

    Decimal(‘4.141’)

    >>>decimal.getcontext().prec

    = 2

    >>>decimal.Decimal(‘1.00’)/decimal.Decimal(‘3.00’)

    Decimal(‘0.33’)

    Python最近还添加了布尔值(预定义的True和False对象实际上是定制后以逻辑结果显示的整数1和0),以及长期以来一直支持的特殊的占位符对象None(它通常用来初始化名字和对象):

    >>>1>1,

    1<2 # Booleans

    (False,

    True)

    >>>bool(‘spam’)

    True

    >>>

    X= None # None placeholder

    >>>print(X)

    None

    >>>

    L= [None] * 100 # Initialize a list of 100

    Nones

    >>>

    L

    [None, None, None,None, … alist of 100

    Nones…]

    10如何破坏代码的灵活性

    内置函数type返回的类型对象是赋给该类型的另一个对象的一个对象,其结果在Python3.0中略有不同,因为类型已经完全和类结合起来了,假设L仍是列表。

    # Inpython 2.6

    >>>

    type(L) # Types:type of L is list type

    object

    >>>

    type(type(L)) #Even types are

    objects

    # In

    python3.0

    >>>type(L)

    # 3.0: type are classes, and vice

    versa

    >>>type(type(L))

    # See chapter 31 for more on class

    types

    除了允许交互地探究对象,这个函数的实际应用是,允许编写代码来检查它所处理的对象的类型。实际上,在Python脚本中至少有3种方法可做到这点:

    >>>if

    type(L) == type([]): # Type testing, if you

    must…

    print(‘yes’)

    yes

    >>>if

    type(L) == List: # Using the type

    name

    print(‘yes’)

    yes

    >>>if

    isinstance(L,list): # Object-oriented

    tests

    print(‘yes’)

    yes

    在Python程序中这样做类型检验基本都是错误的(原因以后澄清)。在代码中检验了特定的类型,实际上破坏了它的灵活性,即限制它只能使用一种类型工作。没有这样的检测,代码也许能够使用整个范围的类型工作。

    在Python中,我们编写对象接口(所支持的操作)而不是类型。不关注特定的类型意味着代码会自动地适应它们中的很多类型:任何具有兼容接口的对象均能够工作,而不管它是什么对象类型,尽管支持类型检测。多态是使用Python的一个关键思想。

    11用户定义的类

    类定义了新的对象类型,扩展了核心类型。加入你希望有一个对象类型对职员进行建模。尽管Python里没有这样特定的核心类型,下边这个用户定义的类或许符合你的需求:

    >>>class

    Worker:

    def

    __init__(self, name, pay): #Initialize when

    created

    self.name

    = name # self is the newobject

    self.pay

    = pay

    def

    lastName(self):

    return

    self.name.split()[-1] #Split string on

    blanks

    def

    giveRaise(self, percent):

    self.pay

    *= (1.0 + percent) #Update pay

    in-place

    这个类定义了一个新的对象的种类,有name和pay两个属性(有时候叫状态信息),也有两个小的行为编写为函数(通常叫做方法)的形式。就像函数那样去调用类,会生成我们新类型的实例,并且类的方法调用时,类的方法自动获取被处理的实例(其中的self参数):

    >>>bob

    = Worker(‘Bob smith’, 50000) # Make two

    instance

    >>>sue

    = Worker(‘Sue Jones’, 60000) # Each has name and pay

    attrs

    >>>bob.lastName()

    # Call method: bob is self

    ‘Smith’

    >>>sue.lastName()

    # sue is the self subject

    ‘Jones’

    >>>sue.giveRaise(.10)

    # Updates sue’s pay

    >>>sue.pay

    66000.0

    隐含的“self”对象是我们把这叫做面向对象模型的原因,即一个类中的函数总有一个隐含的对象。一般来说,尽管这样,基于类的类型是建立在并使用了核心类型的。例如,这里的一个用户定义的Worker对象,是一个字符串和数字(分别为name和pay)的集合,附加了用来处理这两个内置对象的函数。

    关于类更多的知识及其继承机制使Python支持了软件层次,使其可以通过扩展进行定制。我们通过编写新的类来扩展软件,而不是改变目前工作的类。类是Python可选的一个特性,并且与用户编写的类相比,像列表和字典这样的更简单的内置类型往往是更好的工具。

    11.1剩余的内容

    Python脚本中能够处理的所有事情都是某种类型的对象。其他的Python中的类型有的是程序执行相关的对象(如函数、模块、类和编译过的代码),有的是由导入的模块函数实现的,而不是语言语法。后者也更倾向于特定的应用领域的角色,例如,文本模式、数据库接口、网络连接等。

    展开全文
  • 第一章 对象的概念 计算机革命起源机器。编程语言就像那台机器。它不仅我们思维放大的工具与另一种表达媒介,更像我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程...

    第一章 对象的概念

    计算机革命起源机器。编程语言就像是那台机器。它不仅是我们思维放大的工具与另一种表达媒介,更像是我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。

    面向对象编程(Object-Oriented Programming OOP)是一种编程思维方式和编码架构。

    抽象

    所有编程语言都提供抽象机制。从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。这里的“类型”意思是:抽象的内容是什么?汇编语言是对底层机器的轻微抽象。接着出现的“命令式”语言(如 FORTRAN,BASIC 和 C)是对汇编语言的抽象。与汇编相比,这类语言已有了长足的改进,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非问题本身的结构。

    程序员必须要在机器模型(“解决方案空间”)和实际解决的问题模型(“问题空间”)之间建立起一种关联。这个过程既费精力,又脱离编程语言本身的范畴。这使得程序代码很难编写,维护代价高昂。同时还造就了一个副产业“编程方法”学科。

    为机器建模的另一个方法针对待解问题建模。对一些早期语言来说,如 LISP 和 APL,它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG 则将所有问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。

    面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种表达非常具有普遍性,所以不必受限于特定类型的问题。
    我们将问题空间中的元素以及它们在解决方案空间的表示称作“对象”(Object)。当然,还有一些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以当你在阅读描述解决方案的代码时,也是在阅读问题的表述。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是根据运行解决方案的计算机。然而,它仍然与计算机有联系,每个对象都类似一台小计算机:它们有自己的状态并且可以进行特定的操作。这与现实世界的“对象”或者“物体”相似:它们都有自己的特征和行为。

    Smalltalk 作为第一个成功的面向对象并影响了 Java 的程序设计语言 ,Alan Kay 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:

    1. 万物皆对象。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其“发出请求”时执行本身的操作。理论上讲,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。
    2. 程序是一组对象,通过消息传递来告知彼此该做什么。要请求调用一个对象的方法,你需要向该对象发送消息。
    3. 每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
    4. 每个对象都有一种类型。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。
    5. 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收发送给"形状”的消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。

    Grady Booch 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和标识。这意味着对象有自己的内部数据(提供状态)、方法 (产生行为),并彼此区分(每个对象在内存中都有唯一的地址)。

    接口

    亚里士多德(Aristotle)大概是第一个认真研究“类型”的哲学家,他曾提出过“鱼类和鸟类”这样的概念。所有对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。这种思想被首次应用于第一个面向对象编程语言 Simula-67,它在程序中使用基本关键字 class 来引入新的类型(class 和 type 通常可互换使用,有些人对它们进行了进一步区分,他们强调 type 决定了接口,而 class 是那个接口的一种特殊实现方式)。

    Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(Simulate)类似“银行出纳员”这样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号、交易和货币单位等许多"对象”。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。

    因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了 class 关键字。当你看到 “type” 这个词的时候,请同时想到 class;反之亦然。

    创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。

    那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”(Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。“类型”与“接口”的对应关系是面向对象程序设计的基础。

    下面让我们以电灯泡为例:

    Light lt = new Light();
    lt.on();
    

    在这个例子中,类型/类的名称是 Light,可向 Light 对象发出的请求包括打开 on、关闭 off、变得更明亮 brighten 或者变得更暗淡 dim。通过声明一个引用,如 ltnew 关键字,我们创建了一个 Light 类型的对象,再用等号将其赋给引用。

    为了向对象发送消息,我们使用句点符号 .lt 和消息名称 on 连接起来。可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单直观的。

    上图遵循 UML(Unified Modeling Language,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分是要描述的任何数据成员,方法(属于此对象的方法,它们接收任何发送到该对象的消息)在框的底部。通常,只有类的名称和公共方法在 UML 设计图中显示,因此中间部分未显示,如本例所示。如果你只对类名感兴趣,则也不需要显示方法信息。

    服务提供

    在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。

    那么问题来了:我们该选择哪个对象来解决问题呢?例如,你正在开发一个记事本程序。你可能会想到在屏幕输入默认的记事本对象,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象?

    我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。

    封装

    我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否会按照正确的规则来使用,甚至是改变该工具类。只有设定访问控制,才能从根本上阻止这种情况的发生。

    因此,使用访问控制的原因有以下两点:

    1. 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。);

    2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松地完成改造。

    Java 有三个显式关键字来设置类中的访问权限:public(公开),private(私有)和protected(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。

    1. public(公开)表示任何人都可以访问和使用该元素;

    2. private(私有)除了类本身和类内部的方法,外界无法直接访问该元素。private 是类和调用者之间的屏障。任何试图访问私有成员的行为都会报编译时错误;

    3. protected(受保护)类似于 private,区别是子类(下一节就会引入继承的概念)可以访问 protected 的成员,但不能访问 private 成员;

    4. default(默认)如果你不使用前面的三者,默认就是 default 访问权限。default 被称为包访问,因为该权限下的资源可以被同一包(库组件)中其他类的成员访问。

    复用

    一个类经创建和测试后,理应是可复用的。然而很多时候,由于程序员没有足够的编程经验和远见,我们的代码复用性并不强。

    代码和设计方案的复用性是面向对象程序设计的优点之一。我们可以通过重复使用某个类的对象来达到这种复用性。同时,我们也可以将一个类的对象作为另一个类的成员变量使用。新的类可以是由任意数量和任意类型的其他对象构成。这里涉及到“组合”和“聚合”的概念:

    • 组合(Composition)经常用来表示“拥有”关系(has-a relationship)。例如,“汽车拥有引擎”。

    • 聚合(Aggregation)动态的组合


    上图中实心三角形指向“ Car ”表示 组合 的关系;如果是 聚合 关系,可以使用空心三角形。

    使用“组合”关系给我们的程序带来极大的灵活性。通常新建的类中,成员对象会使用 private 访问权限,这样应用程序员则无法对其直接访问。我们就可以在不影响客户代码的前提下,从容地修改那些成员。我们也可以在“运行时"改变成员对象从而动态地改变程序的行为,这进一步增大了灵活性。下面一节要讲到的“继承”并不具备这种灵活性,因为编译器对通过继承创建的类进行了限制。

    在面向对象编程中经常重点强调“继承”。在新手程序员的印象里,或许先入为主地认为“继承应当随处可见”。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑“组合”,因为它更简单灵活,而且设计更加清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。

    继承

    “继承”给面向对象编程带来极大的便利。它在概念上允许我们将各式各样的数据和功能封装到一起,这样便可恰当表达“问题空间”的概念,而不用受制于必须使用底层机器语言。通过使用 class 关键字,这些概念形成了编程语言中的基本单元。
    遗憾的是,这么做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。但我们若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。

    这个图中的箭头从派生类指向基类。正如你将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的消息(或者以不同的方式处理它们)。继承通过基类和派生类的概念来表达这种相似性。基类包含派生自它的类型之间共享的所有特征和行为。创建基类以表示思想的核心。从基类中派生出其他类型来表示实现该核心的不同方式。

    例如,垃圾回收机对垃圾进行分类。基类是“垃圾”。每块垃圾都有重量、价值等特性,它们可以被切碎、熔化或分解。在此基础上,可以通过添加额外的特性(瓶子有颜色,钢罐有磁性)或行为(铝罐可以被压碎)派生出更具体的垃圾类型。此外,一些行为可以不同(纸张的价值取决于它的类型和状态)。使用继承,你将构建一个类型层次结构,来表示你试图解决的某种类型的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基类是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以派生出(继承出)具体类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。

    例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积。类型层次结构体现了形状之间的相似性和差异性。以相同的术语将解决方案转换成问题是有用的,因为你不需要在问题描述和解决方案描述之间建立许多中间模型。通过使用对象,类型层次结构成为了主要模型,因此你可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。事实上,有时候,那些善于寻找复杂解决方案的人会被面向对象设计的简单性难倒。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),而且更重要的是它复制了基类的接口。也就是说,基类对象接收的所有消息也能被派生类对象接收。根据类接收的消息,我们知道类的类型,因此派生类与基类是相同的类型。

    在前面的例子中,“圆是形状”。这种通过继承的类型等价性是理解面向对象编程含义的基本门槛之一。因为基类和派生类都具有相同的基本接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有可执行代码。如果继承一个类而不做其他任何事,则来自基类接口的方法直接进入派生类。这意味着派生类和基类不仅具有相同的类型,而且具有相同的行为,这么做没什么特别意义。

    有两种方法可以区分新的派生类与原始的基类。第一种方法很简单:在派生类中添加新方法。这些新方法不是基类接口的一部分。这意味着基类不能满足你的所有需求,所以你添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,还是要仔细考虑是否在基类中也要有这些额外的方法。这种设计的发现与迭代过程在面向对象程序设计中会经常发生。

    尽管继承有时意味着你要在接口中添加新方法(尤其是在以 extends 关键字表示继承的 Java 中),但并非总需如此。第二种也是更重要地区分派生类和基类的方法是改变现有基类方法的行为,这被称为覆盖 (overriding)。要想覆盖一个方法,只需要在派生类中重新定义这个方法即可。

    "是一个"与"像是一个"的关系

    对于继承可能会引发争论:继承应该只覆盖基类的方法(不应该添加基类中没有的方法)吗?如果这样的话,基类和派生类就是相同的类型了,因为它们具有相同的接口。这会造成,你可以用一个派生类对象完全替代基类对象,这叫作"纯粹替代",也经常被称作"替代原则"。在某种意义上,这是一种处理继承的理想方式。我们经常把这种基类和派生类的关系称为是一个(is-a)关系,因为可以说"圆是一个形状"。判断是否继承,就看在你的类之间有无这种 is-a 关系。

    有时你在派生类添加了新的接口元素,从而扩展接口。虽然新类型仍然可以替代基类,但是这种替代不完美,原因在于基类无法访问新添加的方法。这种关系称为像是一个(is-like-a)关系。新类型不但拥有旧类型的接口,而且包含其他方法,所以不能说新旧类型完全相同。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wqxcPQpl-1586240723254)(…/images/1545764820176.png)]

    以空调为例,假设房间里已经安装好了制冷设备的控制器,即你有了控制制冷设备的接口。想象一下,现在空调坏了,你重新安装了一个既制冷又制热的热力泵。热力泵就像是一个(is-like-a)空调,但它可以做更多。因为当初房间的控制系统被设计成只能控制制冷设备,所以它只能与新对象(热力泵)的制冷部分通信。新对象的接口已经扩展了,现有控制系统却只知道原来的接口,一旦看到这个设计,你就会发现,作为基类的制冷系统不够一般化,应该被重新命名为"温度控制系统",也应该包含制热功能,这样的话,我们就可以使用替代原则了。上图反映了在现实世界中进行设计时可能会发生的事情。

    当你看到替代原则时,很容易会认为纯粹替代是唯一可行的方式,并且使用纯粹替代的设计是很好的。但有些时候,你会发现必须得在派生(扩展)类中添加新方法(提供新的接口)。只要仔细审视,你可以很明显地区分两种设计方式的使用场合。

    多态

    我们在处理类的层次结构时,通常把一个对象看成是它所属的基类,而不是把它当成具体类。通过这种方式,我们可以编写出不局限于特定类型的代码。在上个“形状”的例子中,“方法”(method)操纵的是通用“形状”,而不关心它们是“圆”、“正方形”、“三角形”还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动,因此“方法”向其中的任何代表“形状”的对象发送消息都不必担心对象如何处理信息。

    这样的代码不会受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 例如,你可以通过通用的“形状”基类派生出新的“五角形”形状的子类,而不需要修改通用"形状"基类的方法。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。

    这种能力改善了我们的设计,且减少了软件的维护代价。如果我们把派生的对象类型统一看成是它本身的基类(“圆”当作“形状”,“自行车”当作“车”,“鸬鹚”当作“鸟”等等),编译器(compiler)在编译时期就无法准确地知道什么“形状”被擦除,哪一种“车”在行驶,或者是哪种“鸟”在飞行。这就是关键所在:当程序接收这种消息时,程序员并不想知道哪段代码会被执行。“绘图”的方法可以平等地应用到每种可能的“形状”上,形状会依据自身的具体类型执行恰当的代码。

    如果不需要知道执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 BirdController 对象和通用 Bird 对象中,BirdController 不知道 Bird 的确切类型却还能一起工作。从 BirdController 的角度来看,这是很方便的,因为它不需要编写特别的代码来确定 Bird 对象的确切类型或行为。那么,在调用 move() 方法时是如何保证发生正确的行为(鹅走路、飞或游泳、企鹅走路或游泳)的呢?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5WmynROf-1586240723255)(…/images/1545839316314.png)]

    这个问题的答案,是面向对象程序设计的妙诀:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器产生的函数调用会引起所谓的早期绑定,这个术语你可能从未听说过,不会想过其他的函数调用方式。这意味着编译器生成对特定函数名的调用,该调用会被解析为将执行的代码的绝对地址。

    通过继承,程序直到运行时才能确定代码的地址,因此发送消息给对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用后期绑定的概念。当向对象发送信息时,被调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。

    为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当你向对象发送消息时,对象知道该如何处理这条消息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++ 使用 virtual 关键字。在这些语言中,默认情况下方法不是动态绑定的。在 Java 中,动态绑定是默认行为,不需要额外的关键字来实现多态性。

    为了演示多态性,我们编写了一段代码,它忽略了类型的具体细节,只与基类对话。该代码与具体类型信息分离,因此更易于编写和理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。

    代码示例:

    void doSomething(Shape shape) {
        shape.erase();
        // ...
        shape.draw();
    }
    

    此方法与任何 Shape 对话,因此它与所绘制和擦除的对象的具体类型无关。如果程序的其他部分使用 doSomething() 方法:

        Circle circle = new Circle();
        Triangle triangle = new Triangle();
        Line line = new Line();
        doSomething(circle);
        doSomething(triangle);
        doSomething(line);
    
    

    可以看到无论传入的“形状”是什么,程序都正确的执行了。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fay1SdWB-1586240723256)(…/images/1545841270997.png)]

    这是一个非常令人惊奇的编程技巧。分析下面这行代码:

        doSomething(circle);
    

    当预期接收 Shape 的方法被传入了 Circle,会发生什么。由于 Circle 也是一种 Shape,所
    doSomething(circle) 能正确地执行。也就是说,doSomething() 能接收任意发送给 Shape 的消息。这是完全安全和合乎逻辑的事情。

    这种把子类当成其基类来处理的过程叫做“向上转型”(upcasting)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 doSomething() 代码示例:

        shape.erase();
        // ...
        shape.draw();
    

    我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做…”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能删掉 erase() 和绘制 draw(),你自己去做吧,注意细节。”

    尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用draw() 时执行的代码与为一个 Square 或 Line 调用 draw() 时执行的代码是不同的。但在将 draw() 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 doSomething() 编译代码时,它并不知道自己要操作的准确类型是什么。

    尽管我们确实可以保证最终会为 Shape 调用 erase()draw(),但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢?

    发送消息给对象时,如果程序不知道接收的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(Polymorphism)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好地设计程序。

    单继承结构

    自从 C++ 引入以来,一个 OOP 问题变得尤为突出:是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的(实际上,除 C++ 以外的几乎所有OOP语言中也是这样)。在 Java 中,这个最终基类的名字就是 Object

    Java 的单继承结构有很多好处。由于所有对象都具有一个公共接口,因此它们最终都属于同一个基类。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个基类。从向后兼容的角度看,多继承的方案更符合 C 的模型,而且受限较少。

    对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗?如果从零开始,Java 这样的替代方案会是更好的选择。

    另外,单继承的结构使得垃圾收集器的实现更为容易。这也是 Java 在 C++ 基础上的根本改进之一。

    由于运行期的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了对象类型的情况。这对于系统级操作尤其重要,例如异常处理。同时,这也让我们的编程具有更大的灵活性。

    集合

    通常,我们并不知道解决某个具体问题需要的对象数量和持续时间,以及对象的存储方式。那么我们如何知悉程序在运行时需要分配的内存空间呢?

    在面向对象的设计中,问题的解决方案有些过于轻率:创建一个新类型的对象来引用、容纳其他的对象。当然,我们也可以使用多数编程语言都支持的“数组”(array)。在 Java 中“集合”(Collection)的使用率更高。(也可称之为“容器”,但“集合”这个称呼更通用。)

    “集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。

    还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分,通常被称为 STL(Standard Template Library,标准模板库)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。

    在一些库中,一两个泛型集合就能满足我们所有的需求了,而在其他一些类库(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、栈(Stack)、堆(Heap)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:

    1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。

    2. 不同的集合对某些操作有不同的效率。例如,List 的两种基本类型:ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为,但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。

    我们可以一开始使用 LinkedList 构建程序,在优化系统性能时改用 ArrayList。通过对 List 接口的抽象,我们可以很容易地将 LinkedList 改为 ArrayList。

    9.1 参数化类型(泛型)

    在 Java 5 泛型出来之前,集合中保存的是通用类型 Object。Java 单继承的结构意味着所有元素都基于 Object 类,所以在集合中可以保存任何类型的数据,易于重用。要使用这样的集合,我们先要往集合添加元素。由于 Java 5 版本前的集合只保存 Object,当我们往集合中添加元素时,元素便向上转型成了 Object,从而丢失自己原有的类型特性。这时我们再从集合中取出该元素时,元素的类型变成了 Object。那么我们该怎么将其转回原先具体的类型呢?这里,我们使用了强制类型转换将其转为更具体的类型,这个过程称为对象的“向下转型”。通过“向上转型”,我们知道“圆形”也是一种“形状”,这个过程是安全的。可是我们不能从“Object”看出其就是“圆形”或“形状”,所以除非我们能确定元素的具体类型信息,否则“向下转型”就是不安全的。也不能说这样的错误就是完全危险的,因为一旦我们转化了错误的类型,程序就会运行出错,抛出“运行时异常”(RuntimeException)。(后面的章节会提到) 无论如何,我们要寻找一种在取出集合元素时确定其具体类型的方法。另外,每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。

    以某种方式创建集合,以确认保存元素的具体类型,减少集合元素“向下转型”的开销和可能出现的错误难道不好吗?这种解决方案就是:参数化类型机制(Parameterized Type Mechanism)。

    参数化类型机制可以使得编译器能够自动识别某个 class 的具体类型并正确地执行。举个例子,对集合的参数化类型机制可以让集合仅接受“形状”这种类型的元素,并以“形状”类型取出元素。Java 5 版本支持了参数化类型机制,称之为“泛型”(Generic)。泛型是 Java 5 的主要特性之一。你可以按以下方式向 ArrayList 中添加 Shape(形状):

        ArrayList<Shape> shapes = new ArrayList<>();
    

    泛型的应用,让 Java 的许多标准库和组件都发生了改变。在本书的代码示例中,你也会经常看到泛型的身影。

    10 对象的创建与生命周期

    使用对象时要注意的一个关键问题就是对象的创建和销毁方式。每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时,我们应该及时释放资源,清理内存。

    在简单的编程场景下,对象的清理并不是问题。我们创建对象,按需使用,最后销毁它。然而,情况往往要比这更复杂:
    假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统,也许这些数据不像主要控制功能那样引人注意。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。
    现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。

    对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一,因此它将选择权交给了程序员。为了获得最大的运行时速度,程序员可以在编写程序时,通过将对象放在栈(Stack,有时称为自动变量或作用域变量)或静态存储区域(static storage area)中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。

    然而相对的,我们也牺牲了程序的灵活性。因为在编写代码时,我们必须要弄清楚对象的数量、生存时间还有类型。如果我们要用它来解决一个相当普遍的问题时(如计算机辅助设计、仓库管理或空中交通管制等),限制就太大了。

    第二种方法是在被称为堆(Heap)的内存池中动态地创建对象。在这种方式下,直到运行时才能确定需要多少对象、生命周期和具体类型。什么时候需要,什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟和释放空间通常是一条将栈指针向下移动和一条将栈指针向上移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。

    动态方式有这样一个一般性的逻辑假设:对象趋于复杂,因此额外的内存查找和释放对对象的创建影响不大。此外,更好的灵活性对于问题的解决至关重要。

    Java 使用动态内存分配。每次创建对象时,使用 new 关键字构建该对象的动态实例。这又带来另一个问题:对象的生命周期。较之堆内存,在栈内存中创建对象,编译器能够确定该对象的生命周期并自动销毁它;然而如果你在堆内存创建对象的话,编译器是不知道它的生命周期的。在 C++ 中你必须以编程方式确定何时销毁对象,否则可能导致内存泄漏。Java 的内存管理是建立在垃圾收集器上的,它能自动发现对象不再被使用并释放内存。垃圾收集器的存在带来了极大的便利,它减少了我们之前必须要跟踪的问题和编写相关代码的数量。因此,垃圾收集器提供了更高级别的保险,以防止潜在的内存泄漏问题,这个问题使得许多 C++ 项目没落。

    Java 的垃圾收集器被设计用来解决内存释放的问题(虽然这不包括对象清理的其他方面)。垃圾收集器知道对象什么时候不再被使用并且自动释放内存。结合单继承和仅可在堆中创建对象的机制,Java 的编码过程比用 C++ 要简单得多。我们所要做的决定和要克服的障碍也会少很多!

    异常处理

    自编程语言被发明以来,程序的错误处理一直都是个难题。因为很难设计出一个好的错误处理方案,所以许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。

    异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”(Exception)是一个从出错点“抛出”(thrown)后能被特定类型的异常处理程序捕获(catch)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,异常不像方法返回的错误值和方法设置用来表示发生错误的标志位那样可以被忽略。异常的发生是不会被忽略的,它终究会在某一时刻被处理。

    最后,“异常机制”提供了一种可靠地从错误状况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。

    Java 的异常处理机制在编程语言中脱颖而出。Java 从一开始就内置了异常处理,因此你不得不使用它。这是 Java 语言唯一接受的错误报告方法。如果没有编写适当的异常处理代码,你将会收到一条编译时错误消息。这种有保障的一致性有时会让程序的错误处理变得更容易。值得注意的是,异常处理并不是面向对象的特性。尽管在面向对象的语言中异常通常由对象表示,但是在面向对象语言之前也存在异常处理。

    总结

    面向过程程序包含数据定义和函数调用。要找到程序的意图,你必须要在脑中建立一个模型,弄清函数调用和更底层的概念。这些程序令人困扰,因为它们的表示更多地面向计算机而不是我们要解决的问题,这就是我们在设计程序时需要中间表示的原因。OOP 在面向过程编程的基础上增加了许多新的概念,所以有人会认为使用 Java 来编程会比同等的面向过程编程要更复杂。在这里,我想给大家一个惊喜:通常按照 Java 规范编写的程序会比面向过程程序更容易被理解。

    你看到的是对象的概念,这些概念是站在“问题空间”的(而不是站在计算机角度的“解决方案空间”),以及发送消息给对象以指示该空间中的活动。面向对象编程的一个优点是:设计良好的 Java 程序代码更容易被人阅读理解。由于 Java 类库的复用性,通常程序要写的代码也会少得多。

    OOP 和 Java 不一定适合每个人。评估自己的需求以及与现有方案作比较是很重要的。请充分考虑后再决定是不是选择 Java。如果在可预见的未来,Java 并不能很好的满足你的特定需求,那么你应该去寻找其他替代方案(特别是,我推荐看 Python)。如果你依然选择 Java 作为你的开发语言,我希望你至少应该清楚你选择的是什么,以及为什么选择这个方向。

    展开全文
  • 本篇文章主要内容:构造方法Constructor空指针异常...构造方法类中特殊的方法,通过调用构造方法来完成对象的创建,以及对象属性的初始化操作。构造方法怎么定义,请看以下的语法格式:[修饰符列表]构造方法名(形...

    5a6248719eab539b6cda4d7ca48f5a7b.png

    本篇文章主要内容:构造方法Constructor

    空指针异常

    当实例变量是一个引用

    方法调用时参数的传递问题

    难点解惑

    构造方法Constructor

    什么是构造方法?构造方法怎么定义?构造方法怎么调用?构造方法有什么作用?构造方法可以重载吗?接下来学习一下。

    构造方法是类中特殊的方法,通过调用构造方法来完成对象的创建,以及对象属性的初始化操作。

    构造方法怎么定义,请看以下的语法格式:[修饰符列表]构造方法名(形式参数列表){构造方法体;

    }

    ① 构造方法名和类名一致。

    ② 构造方法用来创建对象,以及完成属性初始化操作。

    ③ 构造方法返回值类型不需要写,写上就报错,包括void也不能写。

    ④ 构造方法的返回值类型实际上是当前类的类型。

    ⑤ 一个类中可以定义多个构造方法,这些构造方法构成方法重载。

    怎么调用构造方法呢,语法格式是:new构造方法名(实际参数列表);接下来,看以下代码:773386bac2cdac8a4426d6cfd43d9864.png调用无参数构造方法

    以上程序运行结果如下图所示:2f8122965126e2a90889d8b83e71210c.png调用无参数构造方法的运行结果

    以上程序的输出结果中虽然第一行看不懂,但起码说明程序是能够正常执行的。我们看到以上Date类当中的代码,并没有发现任何构造方法,为什么可以调用呢?接下来我们来测试一下,把构造方法显示的定义出来:0c0578f7afb70cf5366b9ee6a365230f.png

    81d5b8bbe0b03984f85847805375f542.png

    运行结果如下图所示:91c6d20da563996f8a35f51c3bade930.png测试无参数构造方法

    通过以上程序执行结果确实看到了“newDate()”确实调用了Date类当中的无参数构造方法。再看以下程序:ee7794ce31f19fe61a50e70986ea8bdf.png

    ecbfa826d1a090ac0753680d484eb627.png

    编译报错了,错误信息如下图所示:50ad9cc6e2b4d6328342b4fb1260d995.png编译器错误信息提示

    通过以上的测试,得出这样一个结论(这是java中语法的规定,记住就行):当一个类没有显示的定义任何构造方法的时候,系统默认提供无参数构造方法,当显示的定义构造方法之 后,系统则不再提供无参数构造方法。无参数构造方法又叫做缺省构造器,或者默认构造方法。一般在开发中为了方便编程,建议程序员手动的将无参数构造方法写上,因为不写无参数构造 方法的时候,这个默认的构造方法很有可能就不存在了,另外也是因为无参数构造方法使用的 频率较高。例如以下代码:cd4ec5960c0d9d1e2f8fe8925279c9cb.png

    a82e7e8dc6c30e6e55289f86f4fd7dce.png

    运行结果如下图所示:d205e1f20c15981ac56ca8fe307e868e.png调用所有构造方法

    通过以上的测试可以看出一个类当中可以定义多个构造方法,构造方法是支持重载机制的,具体调用哪个构造方法,那要看调用的时候传递的实际参数列表符合哪个构造方法了。构造方法虽然在返回值类型方面不写任何类型,但它执行结束之后实际上会返回该对象在堆内存当中的内存地址,这个时候可以定义变量接收对象的内存地址,这个变量就是之前所学的“引用”,请看以下代码:4cec6c362de60c9b5c51b5f1b5bab88c.png

    运行结果如下图所示:18f1ceabf926d26bd61a45cecfc621ce.png构造方法的返回值

    以上程序中time1,time2,time3,time4都是引用,输出这些引用的结果是“Date@xxxxx”,对于这个结果目前可以把它等同看做是对象的内存地址(严格来说不是真实的对象内存地址)。通过这个引用就可以访问对象的内存了,例如以下代码:86ad64a4cf4e61c87eccf5528c665a1f.png

    运行结果如下图所示:4042025073033ab057aad770fa60b5aa.png通过“引用”访问属性

    为什么无论通过哪个构造方法创建Date对象,最终的结果都是“0年0月0日”呢?

    这是因为所有的构造方法在执行过程中没有给对象的属性手动赋值,系统则自动赋默认值,实际上大部分情况下我们需要在构造方法中手动的给属性赋值,这本来就是构造方法的主要的职责, 要不然重载多次构造方法就没有意义了,以上的代码应该这样写,请看:88a648b19ffc8cce77da8f044b32a99f.png

    dea5ec2b2905f5478a58f915cd63ace3.png

    运行结果如下图所示:c384f58e1be25258edc5cdbc98a312bc.png通过构造方法给属性赋值

    为什么第一个日期输出的是“0年0月0日”?这是因为调用的是无参数构造方法创建的第一个日期对象,在无参数构造方法中没有给属性赋值,则系统赋默认值,所以年月日都是0。

    那为什么第二个日期输出的是“2008年0月0日”呢?这是因为调用的是“public Date(intyear1){year=year1; }”构造方法创建的第二个日期对象,在这个构造方法当中只是显示的给Date对象的year属性赋值,month和day仍然是系统赋默认值,所以month和day都是0。第三个日期对象是“2008年8月8日”,这是因为在这个对应的构造方法中显示的为year,month,day三个属性都赋值了。

    在编写以上构造方法的时候需要注意变量名的问题,请看下图:3828e751f29d445ee7b4eaa7ff773406.pngjava 遵循就近原则

    通过以上内容的学习得知,构造方法的作用是专门用来创建对象同时给属性赋值的,它的语法很简单,比普通方法还要简单,因为构造方法名和类名一致,还不需要写返回值类型,使用new就可以调用了。在一个类当中可以同时定义多个构造方法,它们之间构成重载关系。这样就做到了在java中你想要什么就new什么,每一次new都会在堆内存中创建对象,并且对象内部的实例变量被初始化了。

    一定要注意,实例变量没有手动赋值的时候系统会默认赋值,但不管是手动赋值还是系统赋默认值,都是在构造方法执行的时候才会进行赋值操作,类加载的时候并不会初始化实例变量的空间,那是因为实例变量是对象级别的变量,没有对象,哪来实例变量,这也是为什么实例变量不能采用“类名”去访问的原因。bcfb45e60385de3f2f4e4d7ce6b652c4.png

    编译报错了:11a6a847ecaee3383916f41eba628a70.png实例变量不能直接采用“类名”访问

    空指针异常

    当一个空的引用去访问实例变量会出现什么问题吗?请看以下代码:947c49a684a2a95385b41610c3c24c3a.png

    dc549cf5329232d81c58c7b112023478.png

    运行结果如下图所示:49f279a2b512ca986037df8f85eabd00.png空指针异常演示

    java.lang.NullPointerException被称为空指针异常,在java编程当中属于很常见的异常,接下来研究一下以上程序执行过程的内存图是如何变化的。请看下图:b99cd9cb330b4ef2592a10031fcf79ec.pngBalloon ball = new Balloon("红色" , "氢气");

    342123402c194d3072806cf013d4cafe.pngball = null;

    以上程序语法正确,编译通过,因为程序在编译阶段检测出“引用ball”属于Balloon类型,在Balloon类中有color属性,所以编译器允许通过ball引用去访问color属性,例如以上代码的ball.color。但是程序在运行阶段会通过ball引用查找堆内存当中的对象,因为color是实例变量,该变量存储在java对象内部,当ball=null执行之后表示“引用ball”不再保存java对象的内存地址,换句话说通过ball引用已经无法找到堆内存当中的java对象了,对于程序来说这个时候就没有办法正常访问了,这种情况下就会发生空指针异常。就好比一个小孩儿放风筝,通过拽线来操控风筝,结果线断了,再拽风筝线的时候,已经无法再操控风筝了,这对于小孩儿来说是一种异常。而java程序中把这种异常叫做NullPointerException。

    总之,当一个“空的引用”去访问“对象相关/实例相关”数据的时候,此时一定会发生空指针异常。

    当实例变量是一个引用

    在以上内容学习的过程当中,其实大家已经接触过实例变量是引用的情况,不知道吧!例如在Student学生类当中有一个属性“Stringname;”,这个属性/实例变量name描述的是学生的姓名,name变量的数据类型是String类型,String类型不属于基本数据类型的范畴,也就是说String类型属于引用数据类型,换句话说String类型应该对应一个String.class文件才对,String是一个类,和我们自己定义的类没什么区别,是这样吗?一起来看看JDK的java源代码:40b052510b67d54598582c5d479f6c71.pngjdk java 源代码位置

    cd8cf2f13932e34b08e2d90593dfe0f2.pngString 类源代码位置

    28fe3a32ade5c91a7d77fd676532b284.pngString 类源代码

    通过查看源代码得知,其实String是一个class,和我们定义的类没有区别,它和基本数据类型还是不一样的(int i=10,i变量是基本数据类型,i变量中存储的就是10),也就是说Stringname=“zhangsan”,实际上name变量中存储的并不是”zhangsan”这个字符串,因为name是一个引用,那name中必然存储的是”zhangsan”字符串对象的内存地址。

    因为之前我们说过引用的概念,什么是引用:引用就是一个变量,只不过该变量中存储的是java对象的内存地址。也就是说,以前所讲内容中使用内存图描述字符串的时候都是有偏差的。我们来修正一下, 请看代码:a2867656d44514fc9dd541a28ac29226.png

    54aea1a3a77d3258d556696ab8fab2f8.png

    以上程序的运行结果请看下图:930eaa11499124531ff3aceb51172ace.png运行结果

    将以上内存结构图画出来,请看:422f02653395995c134f8254d97f1e1f.png修正字符串对象的内存

    通过上图可以看到,Student对象当中的name这个“属性/实例变量”是一个引用,保存的不是”zhangsan”字符串,而是字符串对象的内存地址。(按照实际来说,字符串”zhangsan”是在方法区的字符串常量池当中,这个后期再继续进行修正)。接下来,我们再来看看当属性是其他类型引用的时候,请看代码:4b8d4520fd680e2ed8d6f5337b0064d3.png

    215df1a1347d2585f281cdcba6ff9551.png

    07983c25285d6ca8d69a2b9f20cf4740.png

    运行结果如下图所示:2b8394e7e1dadcf0fd0340c70f2e8def.png运行结果

    以上程序main 方法的内存结构图如下所示:fa466cb8dec1730226f5e1fcd6ceaa21.pngDate d = new Date(1983 , 5 , 6);

    22bc706c898b84adc3421e2b9e7e7408.pngVip v = new Vip(123 , "jack" , d);

    通过以上内容的学习大家掌握当对象的属性是一个引用的时候内存图是怎样的了吗?其实只要记住一点,任何“引用”当中存储一定是对象的内存地址,“引用”不一定只是以局部变量的形式存在,例如以上程序,其中Vip类当中的birth属性就是一个“引用”,它是一个实例变量。当一个对象的属性是引用的时候应该如何访问这个引用所指向的对象呢?这里其实有一个规律,大家记住就行:类当中有什么就可以“.”什么,例如:Vip类中有birth属性,那么就可以v.birth,那么v.birth是Date类型,Date类当中有year属性,那么就可以v.birth.year,你懂了吗?

    方法调用时参数的传递问题(理解)

    方法在调用的时候参数是如何传递的呢?其实在调用的时候参数传递给方法,这个过程就是赋值的过程,参数传递和“赋值规则”完全相同,只不过参数传递在代码上看不见“=”运算符。我们先来深入的研究一下“赋值规则”吧!0606eef5439129e9204632e6834e1f77.png

    f1c12f56cf1f2f69fe7a4b07327ed15e.png

    在以上程序当中,有两个疑问,第一个:a 赋值给 b,a 把什么给了 b?第二个:bird1 赋值给 bird2, bird1 把什么给了 bird2?

    其实 a,b,bird1,bird2 就是 4 个普通的变量,唯一的区别只是 a 和 b 都是基本数据类型的变量,bird1 和 bird2 都是引用数据类型的变量(或者说都是引用),a 变量中保存的那个“值”是 10, bird1 变量中保存的那个“值”是 0x8888(java 对象内存地址),本质上来说 10 和 0x8888 都是“值”, 只不过一个“值”是整数数字,另一个“值”是 java 对象的内存地址,大家不要把内存地址特殊化, 它也是一个普通的值。

    那么“赋值”是什么意思呢,顾名思义,赋值就是把“值”赋上去。a 赋值给 b,本质上不是把 a 给了 b,而是把 a 变量中保存的“值 10”复制了一份给了 b。bird1 赋值给 bird2 本质上不是把 bird1 给了 bird2,而是把 bird1 变量中保存的“值 0x8888”复制了一份给了 bird2。请看以下内存图的变化:5282664a28c1ee3551a370ab47ddfc91.png

    5d0aa8159e3ba68976ae171ebd09efeb.png赋值原理图

    通过以上内存图我们可以看出“赋值”运算的时候实际上和变量的数据类型无关,无论是基本数据类型还是引用数据类型,一律都是将变量中保存的“值”复制一份,然后将复制的这个“值”赋上去。他们的区别在于,如果是基本数据类型则和堆内存当中的对象无关,如果是引用数据类型由于传递的这个值是java对象的内存地址,所以会导致两个引用指向同一个堆内存中的java对象,通过任何一个引用去访问堆内存当中的对象,此对象内存都会受到影响。我们来验证一下,让a++,a应该变成了11,但是b不会变,让bird1.name =  “波利”,然后输出bird2.name的结果肯定也是”波利”,请看代码以及运行结果:3c526f526c7c449b48bf4e79a58f4a71.png

    96b6cc6a65134dee883a256a43d05906.png

    运行结果如下图所示:2529935895e1cf063b3a36ec742cceb4.png赋值原理测试

    上面我就提到了,方法调用时参数的传递和赋值运算符的原理完全相同,那么请大家根据以上内容所学,画出以下程序的内存图,以及推算它们的执行结果:2f7e7feadd26a41a552c188fead8ba2d.png

    f04e01b0c90595b58e0ace1ce346d811.png

    939b8f02635e708da2c0b241f8ddf243.png

    难点解惑

    对于初学者来说,本章节内容是比较艰难的一个章节,主要是因为涉及到程序执行过程中内存的变化。本章节中要想搞明白所有的难点,只需要搞定一点就行,那就是亲手画出每个程序执行时的内存图。

    在这里我要提醒大家的有两点:先要记住Java虚拟机内存管理这块有哪些内存空间,每一个空间中都存储什么;

    代码要遵循一行一行逐行执行,每执行一行则需要在内存方面 发生一些变化。只要大家能把握以上两点,对于空指针异常、实例变量为引用类型、方法调用时参数传递问题等迎刃而解。

    小结

    通过本章节内容的学习,需要大家掌握的是一个类定义好之后,怎么创建对象,对象创建之后怎么去使用这个对象,代码要会写。另外还需要掌握构造方法怎么定义、怎么调用。需要理解构造方法在开发中的作用。

    在本章节内容中大家尤其要对Java虚拟机内存管理这块要有深入的理解。要知道Java虚拟机中的栈和堆各自存储什么,要能够画出程序执行过程中内存的变化。要知道空指针异常是如何发生的,为什么会出现空指针。

    除以上所描述之外,大家还需要掌握方法调用时参数是如何传递的,一定要弄明白这个知识点,在以后的开发中会频繁的进行方法的调用,而方法调用时需要传递参数,参数传递时内存是如何变化的,弄明白这些有助于你对程序运行结果的把控。

    最后附Java零基础视频教程给大家,配合学习效果更佳!!69312ffaed67cd7a598fb02ae77d3127.png

    8b22ab26ef493287a87a82da4f4b89c6.png

    展开全文
  • 对象的基本概念:“万物皆对象”Python中的所有事物都以对象形式存在,从简单的数值类型,到复炸的代码模块,都对象 对象(object):在程序设计方法学中,对象...创建对象:对象类的实例,程序的基本单元;...
  • - Object.create():es6创建对象的另一种方式,可以理解为继承一个对象, 添加的属性在原型下。 * 语法: Object.create(proto, [propertiesObject]) * //方法创建一个新对象,使用现有的对象来提供新创建的对象的...
  • 我们先假设第一次使用该类,这样话new一个对象就可以分为两个过程:加载并初始化类和创建对象。一、类加载过程(第一次使用该类)java使用双亲委派模型来进行类加载,所以在描述类加载过程前,我们先看...
  • 我们先假设第一次使用该类,这样话new一个对象就可以分为两个过程:加载并初始化类和创建对象。一、类加载过程(第一次使用该类)java使用双亲委派模型来进行类加载,所以在描述类加载过程前,我们先看一下...
  • 这个img_enc则是model对象__init()__创建时产生一个类定义在model外面另一个类实现 ``` 这种用法我是见所未见,不知道是什么神仙用法;为此,我特意用python3模拟了一下,结果直接报错,意思类似“img_enc类...
  • 对象的创建

    2020-07-30 17:37:35
    对象的创建 注意: 属性名不强制要求遵守标识符规范,什么乱七八糟的名字都可以使用 但是我们使用时还是尽量按照标识符的规范去做 如果需要使用特殊的属性名,不能采用.的方式来操作,需要使用另一种方式, ...
  • JavaScript对象什么是对象创建对象对象属性语法糖this常见几种对象JSON JavaScript object notation序列化反序列化Symbol字符串对象包装类型Math 数学对象Date 日期对象 什么是对象 数组一组数据有序集合,...
  • 作者:沉默哥http://cnblogs.com/JackPn/p/9386182.htmljava...我们先假设第一次使用该类,这样话new一个对象就可以分为两个过程:加载并初始化类和创建对象。一、类加载过程(第一次使用该类)java使用双亲委...
  • Java对象的创建和使用方法本章节目标:理解构造方法以及重载机制,通过构造方法可以完成对象的创建,并且能够通过引用访问对象的内存,了解Java虚拟机内存管理,能够画出程序执行...怎么创建呢,语法是什么?其实...
  • 字面量语法是什么

    2016-09-07 19:01:01
    例如:NSNumber *number;number = @123445;//整形 number = @'adfasdf';...需要注意的:没有可以用来创建NSMutableArray对象的字面量语法,当然也包括-NSMutableString 、NSMutableDictionary都没有字面量语法
  • 但是,JavaScript中也有构造函数,其使用的语法与Java或其他基于类的语言中创建对象的语法相似,也可以使用自己的构造函数或使用一些类似于Object()、Date()、String()的内置构造函数以创建对象。那么为什么我们平时...
  • 点击上方蓝色字体,选择“设为星标”优质文章,及时送达作者:沉默哥来源:...我们先假设第一次使用该类,这样话new一个对象就可以分为两个过程:加载并初始化类和创建对象。一、类加载过程(第一...
  • 我想知道这些之间区别是什么:1-)JFrame frame = new JFrame();JLabel label = new JLabel("example");frame.add(label);2-)JFrame frame = new JFrame();frame.add(new Label("example"));另外,我们可以使用如下...
  • 欢迎关注微信公众号:深入浅出Java源码作者:沉默哥来源:...我们先假设第一次使用该类,这样话new一个对象就可以分为两个过程:加载并初始化类和创建对象。一、类加载过程(第一次使用该类)java...
  • Ajax ...第一步:创建对象 var xhr = new XMLHttpRequest(); //为了兼容低版本IE浏览器,需要实例化 var xhr = new ActiveXObject('Microsoft,XNLHTTP'); //因此,常用写法: //第一步,实例化
  • javaScript 的对象创建

    2019-10-03 11:56:18
     在传统的面向对象的语言如java中,都会有一个class类,class不是对象,它可以理解为一张图纸。根据这些图纸创建的实例才可以调用的对象。比方说我们有个锤子图纸,它有锤人的功能。我们只有把这个锤子按照图纸...
  • 面向对象语法概念

    2019-09-12 14:13:16
    匿名对象及对象的生命周期 什么是匿名对象 )匿名对象,就是没有名字的对象[创建了一个对象而没有使用变量接收] )该对象存在于堆总处于游离状态:没有任何变量持有引用地址,指向他 )但是匿名对象...
  • 创建对象的方式 字面量创建 var obj = {} 内置构造函数创建 var obj = new Object() 操作对象的语法(点语法) 增: 对象名.你要增加的名字 = 你要增加的值 删: delete 对象.你要删除的名字 改: 对象名.你要修改的名字 ...
  • // b): 构造函数: 本质上就是一个函数,构造函数用来创建对象的 // c): 内置构造函数: js内部提供了一个用来创建对象的函数 2. 如何通过内置构造函数创建对象呢? // 语法: let 对象名 = new Object(); 3.通过...
  • Java语言假设我们只进行面向对象的程序设计。 在本章,我们将看到Java程序的基本组成部分,并体会到在Java中(几乎)一切都对象。 2.1 用引用操纵对象 每种编程语言都有自己的操纵内存中元素的方式。 所有这一切在...
  • 当我发现要写python面向...为什么我只创建是为 a 赋值,就可以使用一些我没写过方法? 可能会有小伙伴说:因为 a 此时个字符串对象呀,当然能够使用字符串方法,至于这些方法,那python事先写好。 好吧,
  • 文章目录什么是什么是对象创建对象 什么是类 人类在认识客观事物时,经常采用一种思维方式就是将众多事物归纳为一些类,抽象出事物本质特征,忽略那些无关非本质特征,从而找出事物共性,把具有具有...
  • 索引关系数据库中用于存放每一条记录一种对象,主要目的加快数据读取速度和完整性检查。建立索引一项技术性要求高工作。一般在数据库设计阶段与数据库结构一道考虑。应用系统性能直接与索引合理...
  • 构造方法一种特殊方法,用来初始化类一个新对象,在创建对象(new 运算符)之后自动调用。Java当中每个类都有一个默认构造方法,并且可以有一个以上构造方法。Java构造方法特点:1、方法名必须和类名...
  • // b): 构造函数: 本质上就是一个函数,构造函数用来创建对象的 // c): 内置构造函数: js内部提供了一个用来创建对象的函数 // 2. 如何通过内置构造函数创建对象呢? // 语法: let 对象名 = new Object(); //...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,106
精华内容 842
关键字:

创建对象的语法是什么