2017年4月26号课堂笔记

2017年4月26号 晴 空气质量:优html

内容:U1知识总结前端

如下为补充或者本身薄弱的环节:(不少是网上搜的,回头慢慢消化)java

1、Tipsnode

ctrl+shift+r : open resource, 打开资源.
它能够打开当前eclipse的工做区中全部(打开的)工程中全部类型的文件,但只限手动编写的文件,不含工程中引用到的
jar包中的类、接口;
ctrl+shift+t : open type, 打开类型.
它能够打开当前eclipse的工做区中全部(打开的)工程中全部java文件,包括jar包中的类和接口.mysql

2、二分查找法nginx

二分查找又称折半查找,优势是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。所以,折半查找方法适用于不常常变更而查找频繁的有序列表。程序员

首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,若是二者相等,则查找成功;不然利用中间位置记录将表分红前、后两个子表,若是中间位置记录的关键字大于查找关键字,则进一步查找前一子表,不然进一步查找后一子表。web

重复以上过程,直到找到知足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。面试

 

二分查找的基本思想是将n个元素分红大体相等的两部分,取a[n/2]与x作比较,若是x=a[n/2],则找到x,算法停止;
若是x<a[n/2],则只要在数组a的左半部分继续搜索x,若是x>a[n/2],则只要在数组a的右半部搜索x.
时间复杂度无非就是while循环的次数!
总共有n个元素,
渐渐跟下去就是n,n/2,n/4,....n/2^k(接下来操做元素的剩余个数),其中k就是循环的次数
因为你n/2^k取整后>=1
即令n/2^k=1
可得k=log2n,(是以2为底,n的对数)
因此时间复杂度能够表示O(h)=O(log2n)
下面提供一段二分查找实现的 伪代码:
BinarySearch(max,min,des)
mid-<(max+min)/2
while(min<=max)
mid=(min+max)/2
if mid=des then
return mid
elseif mid >des then
max=mid-1
else
min=mid+1
return max
 

3、元数据ajax

元数据被定义为:描述数据的数据,对数据及信息资源的描述性信息。

元数据(Metadata)是描述其它数据的数据(data about other data),或者说是用于提供某种资源的有关信息的结构数据(structured data)。
元数据是描述信息资源或数据等对象的数据,其使用目的在于:识别资源;评价资源;追踪资源在使用过程当中的变化;实现简单高效地管理大量网络化数据;实现信息资源的有效发现、查找、一体化组织和对使用资源的有效管理。
 
元数据的 基本特色主要有:
a)元数据一经创建,即可共享。元数据的结构和完整性依赖于信息资源的价值和使用环境;元数据的开发与利用环境每每是一个变化的分布式环境;任何一种格式都不可能彻底知足不一样团体的不一样须要;
b)元数据首先是一种编码体系。元数据是用来描述数字化信息资源,特别是网络信息资源的编码体系,这致使了元数据和传统数据编码体系的根本区别;元数据的最为重要的特征和功能是为数字化信息资源创建一种机器可理解框架。
元数据体系构建了电子政务的逻辑框架和基本模型,从而决定了电子政务的功能特征、运行模式和系统运行的整体性能。电子政务的运做都基于元数据来实现。
其主要 做用有:描述功能、整合功能、控制功能和代理功能。
因为元数据也是数据,所以能够用相似数据的方法在数据库中进行存储和获取。若是提供数据元的组织同时提供描述数据元的元数据,将会使数据元的使用变得准确而高效。用户在使用数据时能够首先查看其元数据以便可以获取本身所需的信息。
 

4、this的用法

 

this关键字主要有三个应用
(1)this调用本类中的属性,也就是类中的成员变量
(2)this调用本类中的其余方法
(3)this调用本类中的其余构造方法,调用时要放在构造方法的首行


Public Class Student {
String name; //定义一个成员变量name
private void SetName(String name) { //定义一个参数(局部变量)name
this.name=name; //将局部变量的值传递给成员变量
}
}

 

应用一:引用成员变量

如上面这段代码中,有一个成员变量name,同时在方法中有一个形式参数,名字也是name,而后在方法中将形式参数name的值传递给成员变量name,虽然咱们能够看明白这个代码的含义,可是做为Java编译器它是怎么判断的呢?

究竟是将形式参数name的值传递给成员变量name,仍是反过来将成员变量name的值传递给形式参数name呢?也就是说,两个变量名字若是相同的话,那么Java如何判断使用哪一个变量?此时this这个关键字就起到做用了。

this这个关键字其表明的就是对象中的成员变量或者方法。也就是说,若是在某个变量前面加上一个this关键字,其指的就是这个对象的成员变量或者方法,而不是指成员方法的形式参数或者局部变量。

为此在上面这个代码中,this.name表明的就是对象中的成员变量,又叫作对象的属性,然后面的name则是方法的形式参数,代码this.name=name就是将形式参数的值传递给成员变量。这就是上面这个代码的具体含义。

通常状况下,在Java语言中引用成员变量或者成员方法都是以对象名.成员变量或者对象名.成员方法的形式。不过有些程序员即便在没有相同变量的时候,也喜欢使用this.成员变量的形式来引用变量,这主要是从便于代码的阅读考虑的。

一看到这个this关键字就知道如今引用的变量是成员变量或者成员方法,而不是局部变量。这无形中就提升了代码的阅读性。不过话说回来,这是this关键字在Java语言中的最简单的应用。从这个应用中,咱们能够看出this关键字其表明的就是对象的名字。

其实若是是局部变量的话,也是相同的道理。如在上面的代码中,name不是形式参数,而是一个局部变量。此时Java也会遇到相同的疑惑,即变量名name表明的究竟是局部变量仍是形式参数?name=name到底表明的是什么含义?根据局部变量的做用域,在方法内部,若是局部变量与成员变量同名的话,那么是以局部变量为准。但是在name=name这个赋值语句中,将局部变量的值赋值给本身,显然并非很合适。

根据代码的含义,原本的意思应该是将局部变量赋值给成员变量。为了更清晰的表达这个含义,为此最好采用以下的书写格式this.name=name。这里的this关键字含义就是对象名student,为此this.name就表示student.name。

 

应用二:调用类的构造方法

public class Student { //定义一个类,类的名字为student。
public Student() { //定义一个方法,名字与类相同故为构造方法
this(“Hello!”);
}
public Student(String name) { //定义一个带形式参数的构造方法
}
}


this关键字除了能够调用成员变量以外,还能够调用构造方法。在一个Java类中,其方法能够分为成员方法和构造方法两种。

构造方法是一个与类同名的方法,在Java类中必须存在一个构造方法。若是在代码中没有显示的体现构造方法的话,那么编译器在编译的时候会自动添加一个没有形式参数的构造方法。这个构造方法跟普通的成员方法仍是有不少不一样的地方。如构造方法一概是没有返回值的,并且也不用void关键字来讲明这个构造方法没有返回值。而普通的方法能够有返回值、也能够没有返回值,程序员能够根据本身的须要来定义。不过若是普通的方法没有返回值的话,那么必定要在方法定义的时候采用void关键字来进行说明。

其次构造方法的名字有严格的要求,即必须与类的名字相同。也就是说,Java编译器发现有个方法与类的名字相同才把其看成构造方法来对待。而对于普通方法的话,则要求不可以与类的名字相同,并且多个成员方法不可以采用相同的名字。在一个类中能够存在多个构造方法,这些构造方法都采用相同的名字,只是形式参数不一样。Java语言就凭形式参数不一样来判断调用哪一个构造方法。

在上面这段代码中,定义了两个构造方法,一个带参数,另外一个没有带参数。构造方法都不会有返回值,不过因为构造方法的特殊性,为此没必要要在构造方法定义时带上void关键字来讲明这个问题。在第一个没有带参数的构造方法中,使用了this(“Hello!”)这句代码,这句代码表示什么含义呢?

在构造方法中使this关键字表示调用类中的构造方法。若是一个类中有多个构造方法,由于其名字都相同,跟类名一致,那么这个this究竟是调用哪一个构造方法呢?其实,这跟采用其余方法引用构造方法同样,都是经过形式参数来调用构造方法的。如上例中,this关键字后面加上了一个参数,那么就表示其引用的是带参数的构造方法。

若是如今有三个构造方法,分别为不带参数、带一个参数、带两个参数。那么Java编译器会根据所传递的参数数量的不一样,来判断该调用哪一个构造方法。从上面示例中能够看出,this关键字不只能够用来引用成员变量,并且还能够用来引用构造方法。

不过若是要使用这种方式来调用构造方法的话,有一个语法上的限制。通常来讲,利用this关键字来调用构造方法,只有在无参数构造方法中第一句使用this调用有参数的构造方法。不然的话,翻译的时候,就会有错误信息。这跟引用成员变量不一样。若是引用成员变量的话,this关键字是没有位置上的限制的。若是不熟悉这个限制的话,那么仍是老老实实的采用传统的构造方法调用方式为好。虽然比较麻烦,可是至少不会出错。


应用三:返回对象的值
this关键字除了能够引用变量或者成员方法以外,还有一个重大的做用就是返回类的引用。如在代码中,能够使用return this,来返回某个类的引用。此时这个this关键字就表明类的名称。如代码在上面student类中,那么代码表明的含义就是return student。可见,这个this关键字除了能够引用变量或者成员方法以外,还能够做为类的返回值,这才是this关键字最引人注意的地方。

 

5、接口和抽象类区别(面试题)

abstract class和interface是Java语言中对于抽象类定义进行支持的两种机制,正是因为这两种机制的存在,才赋予了Java强大的面向对象能力。 abstract class和interface之间在对于抽象类定义的支持方面具备很大的类似性,甚至能够相互替换,所以不少开发者在进行抽象类定义时对于 abstract class和interface的选择显得比较随意。

其实,二者之间仍是有很大的区别的,对于它们的选择甚至反映出对于问题领域本质的理解、对于设计意图的理解是否正确、合理。本文将对它们之间的区别进行一番剖析,试图给开发者提供一个在两者之间进行选择的依据。

(一)理解抽象类

abstract class和interface在Java语言中都是用来进行抽象类(本文中的抽象类并不是从abstract class翻译而来,它表示的是一个抽象体,而abstract class为Java语言中用于定义抽象类的一种方法,请读者注意区分)定义的,那么什么是抽象类,使用抽象类能为咱们带来什么好处呢?

在面向对象的概念中,咱们知道全部的对象都是经过类来描绘的,可是反过来却不是 这样。并非全部的类都是用来描绘对象的,若是一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类每每用来表征咱们在对问题领 域进行分析、设计中得出的抽象概念,是对一系列看上去不一样,可是本质上相同的具体概念的抽象。

好比:若是咱们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形 这样一些具体概念,它们是不一样的,可是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是由于抽象的概念在问题 领域没有对应的具体概念,因此用以表征抽象概念的抽象类是不可以实例化的。

在面向对象领域,抽象类主要用来进行类型隐藏。咱们能够构造出一个固定的一组行 为的抽象描述,可是这组行为却可以有任意个可能的具体实现方式。这个抽象描述就是抽象类,而这一组任意个可能的具体实现则表现为全部可能的派生类。模块可 以操做一个抽象体。因为模块依赖于一个固定的抽象体,所以它能够是不容许修改的;同时,经过从这个抽象体派生,也可扩展此模块的行为功能。熟悉OCP的读 者必定知道,为了可以实现面向对象设计的一个最核心的原则OCP(Open-Closed Principle),抽象类是其中的关键所在。

 

(二)从语法定义层面看abstract class和interface

在语法层面,Java语言对于abstract class和interface给出了不一样的定义方式,下面以定义一个名为Demo的抽象类为例来讲明这种不一样。使用abstract class的方式定义Demo抽象类的方式以下:

 

java 代码
  1. abstract class Demo {    
  2.   
  3. abstract void method1();    
  4.   
  5. abstract void method2();    
  6.   
  7. …    
  8.   
  9. }    

 

 

使用interface的方式定义Demo抽象类的方式以下:

 

java 代码
  1. interface Demo {    
  2.   
  3. void method1();    
  4.   
  5. void method2();    
  6.   
  7. …    
  8.   
  9. }    

 

 

在abstract class方式中,Demo能够有本身的数据成员,也能够有非abstarct的成员方法,而在interface方式的实现中,Demo只可以有静态的 不能被修改的数据成员(也就是必须是static final的,不过在interface中通常不定义数据成员),全部的成员方法都是abstract的。从某种意义上说,interface是一种特殊 形式的abstract class。

从编程的角度来看,abstract class和interface均可以用来实现"design by contract"的思想。可是在具体的使用上面仍是有一些区别的。

首先,abstract class在Java语言中表示的是一种继承关系,一个类只能使用一次继承关系。可是,一个类却能够实现多个interface。也许,这是Java语言的设计者在考虑Java对于多重继承的支持方面的一种折中考虑吧。

其次,在abstract class的定义中,咱们能够赋予方法的默认行为。可是在interface的定义中,方法却不能拥有默认行为,为了绕过这个限制,必须使用委托,可是这会 增长一些复杂性,有时会形成很大的麻烦。

在抽象类中不能定义默认行为还存在另外一个比较严重的问题,那就是可能会形成维护上的 麻烦。由于若是后来想修改类的界面(通常经过abstract class或者interface来表示)以适应新的状况(好比,添加新的方法或者给已用的方法中添加新的参数)时,就会很是的麻烦,可能要花费不少的时 间(对于派生类不少的状况,尤其如此)。可是若是界面是经过abstract class来实现的,那么可能就只须要修改定义在abstract class中的默认行为就能够了。

一样,若是不能在抽象类中定义默认行为,就会致使一样的方法实现出如今该抽象类 的每个派生类中,违反了"one rule,one place"原则,形成代码重复,一样不利于之后的维护。所以,在abstract class和interface间进行选择时要很是的当心。

 

(三)从设计理念层面看abstract class和interface

上面主要从语法定义和编程的角度论述了abstract class和interface的区别,这些层面的区别是比较低层次的、非本质的。本文将从另外一个层面:abstract class和interface所反映出的设计理念,来分析一下两者的区别。做者认为,从这个层面进行分析才能理解两者概念的本质所在。

前面已经提到过,abstarct class在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is a"关系,即父类和派生类在概念本质上应该是相同的。对于interface 来讲则否则,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已。为了使 论述便于理解,下面将经过一个简单的实例进行说明。

考虑这样一个例子,假设在咱们的问题领域中有一个关于Door的抽象概念,该Door具备执行两个动做open和close,此时咱们能够经过abstract class或者interface来定义一个表示该抽象概念的类型,定义方式分别以下所示:

 

使用abstract class方式定义Door:

 

java 代码

 

  1. abstract class Door {    
  2.   
  3. abstract void open();    
  4.   
  5. abstract void close();    
  6.   
  7. }    

使用interface方式定义Door:

 

java 代码
  1. interface Door {    
  2.   
  3. void open();    
  4.   
  5. void close();    
  6.   
  7. }    

 

 

其余具体的Door类型能够extends使用abstract class方式定义的Door或者implements使用interface方式定义的Door。看起来好像使用abstract class和interface没有大的区别。

若是如今要求Door还要具备报警的功能。咱们该如何设计针对该例子的类结构呢(在 本例中,主要是为了展现abstract class和interface反映在设计理念上的区别,其余方面无关的问题都作了简化或者忽略)下面将罗列出可能的解决方案,并从设计理念层面对这些不 同的方案进行分析。

解决方案一:

简单的在Door的定义中增长一个alarm方法,以下:

 

java 代码

 

 

或者

 

java 代码
  1. interface Door {    
  2.   
  3. void open();    
  4.   
  5. void close();    
  6.   
  7. void alarm();    
  8.   
  9. }    

 

 

那么具备报警功能的AlarmDoor的定义方式以下:

 

java 代码
  1. class AlarmDoor extends Door {    
  2.   
  3. void open() { … }    
  4.   
  5. void close() { … }    
  6.   
  7. void alarm() { … }    
  8.   
  9. }    

 

 

或者

 

java 代码
  1. class AlarmDoor implements Door {    
  2.   
  3. void open() { … }    
  4.   
  5. void close() { … }    
  6.   
  7. void alarm() { … }    
  8.   
  9. }    

 

 

这种方法违反了面向对象设计中的一个核心原则ISP(Interface Segregation Priciple),在Door的定义中把Door概念自己固有的行为方法和另一个概念"报警器"的行为方法混在了一块儿。这样引发的一个问题是那些仅仅 依赖于Door这个概念的模块会由于"报警器"这个概念的改变(好比:修改alarm方法的参数)而改变,反之依然。

解决方案二:

既然open、close和alarm属于两个不一样的概念,根据ISP原则应该把它 们分别定义在表明这两个概念的抽象类中。定义方式有:这两个概念都使用abstract class方式定义;两个概念都使用interface方式定义;一个概念使用abstract class方式定义,另外一个概念使用interface方式定义。

显然,因为Java语言不支持多重继承,因此两个概念都使用abstract class方式定义是不可行的。后面两种方式都是可行的,可是对于它们的选择却反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理。咱们一一来分析、说明。

若是两个概念都使用interface方式来定义,那么就反映出两个问题:

一、咱们可能没有理解清楚问题领域,AlarmDoor在概念本质上究竟是Door仍是报警器?

二、若是咱们对于问题领域的理解没有问题,好比:咱们经过对于问题领域的分析发现 AlarmDoor在概念本质上和Door是一致的,那么咱们在实现时就没有可以正确的揭示咱们的设计意图,由于在这两个概念的定义上(均使用 interface方式定义)反映不出上述含义。

若是咱们对于问题领域的理解是:AlarmDoor在概念本质上是Door,同 时它有具备报警的功能。咱们该如何来设计、实现来明确的反映出咱们的意思呢?前面已经说过,abstract class在Java语言中表示一种继承关系,而继承关系在本质上是"is a"关系。因此对于Door这个概念,咱们应该使用abstarct class方式来定义。另外,AlarmDoor又具备报警功能,说明它又可以完成报警概念中定义的行为,因此报警概念能够经过interface方式定 义。以下所示:

 

java 代码
  1. abstract class Door {    
  2.   
  3. abstract void open();    
  4.   
  5. abstract void close();    
  6.   
  7. }    

 

java 代码
  1. interface Alarm {    
  2.   
  3. void alarm();    
  4.   
  5. }    

 

java 代码
  1. class AlarmDoor extends Door implements Alarm {    
  2.   
  3. void open() { … }    
  4.   
  5. void close() { … }    
  6.   
  7. void alarm() { … }    
  8.   
  9. }    

 

 

这种实现方式基本上可以明确的反映出咱们对于问题领域的理解,正确的揭示咱们的设计 意图。其实abstract class表示的是"is a"关系,interface表示的是"like a"关系,你们在选择时能够做为一个依据,固然这是创建在对问题领域的理解上的,好比:若是咱们认为AlarmDoor在概念本质上是报警器,同时又具备 Door的功能,那么上述的定义方式就要反过来了。

abstract class和interface是Java语言中的两种定义抽象类的方式,它们之间有很大的类似性。可是对于它们的选择却又每每反映出对于问题领域中的概 念本质的理解、对于设计意图的反映是否正确、合理,由于它们表现了概念间的不一样的关系(虽然都可以实现需求的功能)。这其实也是语言的一种的惯用法。

 

 (四)总结几句话来讲:

一、抽象类和接口都不能直接实例化,若是要实例化,抽象类变量必须指向实现全部抽象方法的子类对象,接口变量必须指向实现全部接口方法的类对象。

二、抽象类要被子类继承,接口要被类实现。

三、接口只能作方法申明,抽象类中能够作方法申明,也能够作方法实现

四、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。

五、抽象类里的抽象方法必须所有被子类所实现,若是子类不能所有实现父类抽象方法,那么该子类只能是抽象类。一样,一个实现接口的时候,如不能所有实现接口方法,那么该类也只能为抽象类。

六、抽象方法只能申明,不能实现。abstract void abc();不能写成abstract void abc(){}。

七、抽象类里能够没有抽象方法

八、若是一个类里有抽象方法,那么这个类只能是抽象类

九、抽象方法

 

6、自定义异常类

转载自:http://blog.csdn.net/csdn_gia/article/details/53032248

 sun提供了不少的异常类给咱们用于描述程序中各类的不正常状况,可是sun 给我
提供异常类还不足以描述咱们现实生活中全部不正常状况,那么这时候咱们就须要
自定义异常类。

需求: 模拟feiQ上线的时候,若是没有插上网线,那么就抛出一个没有插上网线的异常,
若是已经插上了网上,那么就正常显示好友列表。

自定义异常类的步骤:  自定义一个类继承Exception便可。

 

[java]  view plain  copy
 
  1. //自定义了一个没有网线的异常类了。  
  2. class NoIpException extends Exception{  
  3.   
  4.   
  5.     public NoIpException(String message){  
  6.         super(message);  //调用了Exception一个参数的构造函数。  
  7.     }  
  8.   
  9. }  
  10.   
  11.   
  12.   
  13. class Demo2   
  14. {  
  15.     public static void main(String[] args)   
  16.     {  
  17.         String ip = "192.168.10.100";  
  18.         ip = null;  
  19.         try{  
  20.             feiQ(ip);  // 若是调用了一个声明抛出异常类型的方法,那么调用者必需要处理。  
  21.           
  22.           
  23.         }catch(NoIpException e){  
  24.             e.printStackTrace();  
  25.             System.out.println("立刻插上网线!");  
  26.         }  
  27.           
  28.   
  29.     }  
  30.   
  31.   
  32.     public static void feiQ(String ip) throws NoIpException{  
  33.         if(ip==null){  
  34.             throw new  NoIpException("没有插网线啊,小白!");  
  35.         }  
  36.         System.out.println("正常显示好友列表..");  
  37.     }  
  38.   
  39.   
  40. }  


需求:模拟你去吃木桶饭,若是带钱少于了10块,那么就抛出一个没有带够钱的异常对象,
若是带够了,那么就能够吃上香喷喷的地沟油木桶饭.

 

 

[java]  view plain  copy
 
    1. //定义没钱的异常  
    2. class NoMoneyException extends Exception {  
    3.   
    4.     public NoMoneyException(String message){  
    5.         super(message);  
    6.     }  
    7.   
    8. }  
    9.   
    10.   
    11.   
    12. class Demo3   
    13. {  
    14.     public static void main(String[] args)   
    15.     {  
    16.         //System.out.println("Hello World!");  
    17.         try{  
    18.             eat(9);  
    19.   
    20.         }catch(NoMoneyException e){  
    21.             e.printStackTrace();  
    22.             System.out.println("跟我洗碗一个月!!");  
    23.         }  
    24.     }  
    25.   
    26.   
    27.     public static void eat(int money) throws NoMoneyException{  
    28.         if(money<10){  
    29.             throw new NoMoneyException("吃霸王餐");  
    30.         }  
    31.         System.out.println("吃上了香喷喷的地沟油木桶饭!!");  
    32.     }  
    33. }  

 

 

 

7、Entry

转载自:http://www.cnblogs.com/guanjie20/p/3769772.html

Map是java中的接口,Map.Entry是Map的一个内部接口。

Map提供了一些经常使用方法,如keySet()、entrySet()等方法。

keySet()方法返回值是Map中key值的集合;entrySet()的返回值也是返回一个Set集合,此集合的类型为Map.Entry。

Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。接口中有getKey(),getValue方法。

         

        由以上能够得出,遍历Map的经常使用方法:

       1.  Map map = new HashMap();

           Irerator iterator = map.entrySet().iterator();

           while(iterator.hasNext()) {

                   Map.Entry entry = iterator.next();

                   Object key = entry.getKey();

                   //

           }

       2.Map map = new HashMap(); 

           Set  keySet= map.keySet();

           Irerator iterator = keySet.iterator;

           while(iterator.hasNext()) {

                   Object key = iterator.next();

                   Object value = map.get(key);

                   //

           }

 

       另外,还有一种遍历方法是,单纯的遍历value值,Map有一个values方法,返回的是value的Collection集合。经过遍历collection也能够遍历value,如

      Map map = new HashMap();

      Collection c = map.values();

      Iterator iterator = c.iterator();

      while(iterator.hasNext()) {

             Object value = iterator.next(); 

     }

 

 

8、GC工做机制详解

转载自:http://blog.csdn.net/tonytfjing/article/details/44278233

 

题外话:最近在应聘阿里2015暑期实习,感触颇多。机会老是留给有准备的人的,因此日常必定要注意知识的巩固和积累。知识的深度也要有必定的理解,不比别人知道的多,公司干吗选你?关于JVM和GC,我相信学Java的绝大部分人都听过,不少公司的面试官都爱问,一开始我也很头痛,问这么底层干什么,因此我每次面试也只是看看答案敷衍了事。最近面完阿里感受真不能这样,知识不只要知其然,还要知其因此然。其实弄懂了JVM和GC,对咱们理解不少java知识都有帮助。网上有不少关于GC和JVM的文章,这篇博文主要是根据我最近看《深刻理解Java虚拟机》的一些体会总结出来的,但愿对新手有些帮助,也欢迎大牛拍砖。文章主要分为如下四个部分

JVM结构、内存分配、垃圾回收算法、垃圾收集器。下面咱们一一来看。

1、JVM结构

根据《java虚拟机规范》规定,JVM的基本结构通常以下图所示:

从左图可知,JVM主要包括四个部分:

1.类加载器(ClassLoader):在JVM启动时或者在类运行时将须要的class加载到JVM中。(右图表示了从java源文件到JVM的整个过程,可配合理解。 关于类的加载机制,能够参考http://blog.csdn.net/tonytfjing/article/details/47212291

2.执行引擎:负责执行class文件中包含的字节码指令(执行引擎的工做机制,这里也不细说了,这里主要介绍JVM结构);

3.内存区(也叫运行时数据区):是在JVM运行的时候操做所分配的内存区。运行时内存区主要能够划分为5个区域,如图:

  • 方法区(Method Area):用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),因此你们不要搞混淆了。方法区还包含一个运行时常量池。
  • java堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域(后面解释)。从存储的内容咱们能够很容易知道,方法区和堆是被全部java线程共享的。
  • java栈(Stack):java栈老是和线程关联在一块儿,每当建立一个线程时,JVM就会为这个线程建立一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就建立一个栈帧,用于存储局部变量表、操做栈、方法返回值等。每个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。因此java栈是现成私有的。
  • 程序计数器(PC Register):用于保存当前线程执行的内存地址。因为JVM程序是多线程执行的(线程轮流切换),因此为了保证线程切换回来后,还能恢复到原先状态,就须要一个独立的计数器,记录以前中断的地方,可见程序计数器也是线程私有的。
  • 本地方法栈(Native Method Stack):和java栈的做用差很少,只不过是为JVM使用到的native方法服务的。

4.本地方法接口:主要是调用C或C++实现的本地方法及返回结果。

2、内存分配

我以为了解垃圾回收以前,得先了解JVM是怎么分配内存的,而后识别哪些内存是垃圾须要回收,最后才是用什么方式回收。

Java的内存分配原理与C/C++不一样,C/C++每次申请内存时都要malloc进行系统调用,而系统调用发生在内核空间,每次都要中断进行切换,这须要必定的开销,而Java虚拟机是先一次性分配一块较大的空间,而后每次new时都在该空间上进行分配和释放,减小了系统调用的次数,节省了必定的开销,这有点相似于内存池的概念;二是有了这块空间事后,如何进行分配和回收就跟GC机制有关了。

java通常内存申请有两种:静态内存和动态内存。很容易理解,编译时就可以肯定的内存就是静态内存,即内存是固定的,系统一次性分配,好比int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,好比java对象的内存空间。根据上面咱们知道,java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存天然就跟着回收了。因此这几个区域的内存分配与回收是肯定的,咱们不须要管的。可是java堆和方法区则不同,咱们只有在程序运行期间才知道会建立哪些对象,因此这部份内存的分配和回收都是动态的。通常咱们所说的垃圾回收也是针对的这一部分。

总之Stack的内存管理是顺序分配的,并且定长,不存在内存回收问题;而Heap 则是为java对象的实例随机分配内存,不定长度,因此存在内存分配和回收的问题;

3、垃圾检测、回收算法

垃圾收集器通常必须完成两件事:检测出垃圾;回收垃圾。怎么检测出垃圾?通常有如下几种方法:

引用计数法:给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。

好了,问题来了,若是我有两个对象A和B,互相引用,除此以外,没有其余任何对象引用它们,实际上这两个对象已经没法访问,便是咱们说的垃圾对象。可是互相引用,计数不为0,致使没法回收,因此还有另外一种方法:

可达性分析算法:以根集对象为起始点进行搜索,若是有对象不可达的话,便是垃圾对象。这里的根集通常包括java栈中引用的对象、方法区常良池中引用的对象

本地方法中引用的对象等。

总之,JVM在作垃圾回收的时候,会检查堆中的全部对象是否会被这些根集对象引用,不可以被引用的对象就会被垃圾收集器回收。通常回收算法也有以下几种:

1.标记-清除(Mark-sweep)

算法和名字同样,分为两个阶段:标记和清除。标记全部须要回收的对象,而后统一回收。这是最基础的算法,后续的收集算法都是基于这个算法扩展的。

不足:效率低;标记清除以后会产生大量碎片。效果图以下:

2.复制(Copying)

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另一个区域中。此算法每次只处理正在使用中的对象,所以复制成本比较小,同时复制过去之后还能进行相应的内存整理,不会出现“碎片”问题。固然,此算法的缺点也是很明显的,就是须要两倍内存空间。效果图以下:

3.标记-整理(Mark-Compact)

此算法结合了“标记-清除”和“复制”两个算法的优势。也是分两阶段,第一阶段从根节点开始标记全部被引用对象,第二阶段遍历整个堆,把清除未标记对象而且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。效果图以下:

(1,2,3 图文摘自 http://pengjiaheng.iteye.com/blog/520228,感谢原做者。)

4.分代收集算法

这是当前商业虚拟机经常使用的垃圾收集算法。分代的垃圾回收策略,是基于这样一个事实:不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的收集方式,以便提升回收效率。

为何要运用分代垃圾回收策略?在java程序运行的过程当中,会产生大量的对象,因每一个对象所能承担的职责不一样所具备的功能不一样因此也有着不同的生命周期,有的对象生命周期较长,好比Http请求中的Session对象,线程,Socket链接等;有的对象生命周期较短,好比String对象,因为其不变类的特性,有的在使用一次后便可回收。试想,在不进行对象存活时间区分的状况下,每次垃圾回收都是对整个堆空间进行回收,那么消耗的时间相对会很长,并且对于存活时间较长的对象进行的扫描工做等都是徒劳。所以就须要引入分治的思想,所谓分治的思想就是因地制宜,将对象进行代的划分,把不一样生命周期的对象放在不一样的代上使用不一样的垃圾回收方式。

如何划分?将对象按其生命周期的不一样划分红:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)。其中持久代主要存放的是类信息,因此与java对象的回收关系不大,与回收息息相关的是年轻代和年老代。这里有个比喻很形象

“假设你是一个普通的 Java 对象,你出生在 Eden 区,在 Eden 区有许多和你差很少的小兄弟、小姐妹,能够把 Eden 区当成幼儿园,在这个幼儿园里你们玩了很长时间。Eden 区不能无休止地放大家在里面,因此当年纪稍大,你就要被送到学校去上学,这里假设从小学到高中都称为 Survivor 区。开始的时候你在 Survivor 区里面划分出来的的“From”区,读到高年级了,就进了 Survivor 区的“To”区,中间因为学习成绩不稳定,还常常来回折腾。直到你 18 岁的时候,高中毕业了,该去社会上闯闯了。因而你就去了年老代,年老代里面人也不少。在年老代里,你生活了 20 年 (每次 GC 加一岁),最后寿终正寝,被 GC 回收。有一点没有提,你在年老代遇到了一个同窗,他的名字叫爱德华 (慕光之城里的帅哥吸血鬼),他以及他的家族永远不会死,那么他们就生活在永生代。”

 

具体区域能够经过VisualVM中的VisaulGC插件查看,如图(openjdk 1.7):

年轻代:是全部新对象产生的地方。年轻代被分为3个部分——Enden区和两个Survivor区(From和to)当Eden区被对象填满时,就会执行Minor GC。并把全部存活下来的对象转移到其中一个survivor区(假设为from区)。Minor GC一样会检查存活下来的对象,并把它们转移到另外一个survivor区(假设为to区)。这样在一段时间内,总会有一个空的survivor区。通过屡次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。一般这是在年轻代有资格提高到年老代前经过设定年龄阈值来完成的。须要注意,Survivor的两个区是对称的,没前后关系,from和to是相对的。

年老代:在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,能够说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,由于那些对于这些回收战场上的老兵来讲是小儿科。一般会在老年代内存被占满时将会触发Full GC,回收整个堆内存。

持久代:用于存放静态文件,好比java类、方法等。持久代对垃圾回收没有显著的影响。 

分代回收的效果图以下:

我这里之因此最后讲分代,是由于分代里涉及了前面几种算法。年轻代:涉及了复制算法;年老代:涉及了“标记-整理(Mark-Sweep)”的算法。

4、垃圾收集器

垃圾收集算法是内存回收的方法论,而实现这些方法论的则是垃圾收集器。不一样厂商不一样版本JVM所提供的垃圾收集器可能不一样,这里参照《深刻理解Java虚拟机》说的是JDK1.7版本Hotspot虚拟机,关于垃圾收集器有篇博文总结的不错,我就不说了,详见:http://blog.csdn.net/java2000_wl/article/details/8030172

总结

虽然我不认为学习java必须去了解Java底层的实现,可是我想若是你更加理解JVM和GC的话,你就会更加理解Java,在之后的学习和工做中绝对受益不浅。毕竟咱们的目标不是刷墙工,不是搬运工,而是开发攻城狮啊!

 

 

9、高并发(找到两篇文章先存下来,没太懂的说,感受本身功力差不少)

转载1:http://www.oschina.net/news/73476/programmer-high-concurrency

简单理解下高并发:

高并发是指在同一个时间点,有不少用户同时的访问URL地址,好比:淘宝的双11,双12,就会产生高并发,如贴吧的爆吧,就是恶意的高并发请 求,也就是DDOS攻击,再屌丝点的说法就像玩撸啊撸被ADC暴击了同样,那伤害你懂得(若是你看懂了,这个说法说明是正在奔向人生巅峰的屌丝。

高并发会来带的后果

  • 服务端:

    • 致使站点服务器/DB服务器资源被占满崩溃,数据的存储和更新结果和理想的设计是不同的,好比:出现重复的数据记录,屡次添加了用户积分等。

  • 用户角度:

    • 尼玛,这么卡,老子来参加活动的,刷新了仍是这样,垃圾网站,不再来了。

  • 个人经历:
    在作公司产品网站的过程当中,常常会有这样的需求,好比什么搞个活动专题,抽奖,签到,搞个积分竞拍等等,若是没有考虑到高并发下的数据处理,那就Game Over了,很容易致使抽奖被多抽走,签到会发现一个用户有多条记录,签到一次得到了得到了多积分,等等,各类超出正常逻辑的现象,这就是作产品网站必须 考虑的问题,由于这些都是面向大量用户的,而不是像作ERP管理系统,OA系统那样,只是面向员工。

下面我进行实例分析,简单粗暴,动态分析,纯属本人我的经验分享,若有说错,或者有更好的建议或者意见的请留言,你们一块儿成长。

并发下的数据处理:

经过表设计,如:记录表添加惟一约束,数据处理逻辑使用事物防止并发下的数据错乱问题
经过服务端锁进程防止包并发下的数据错乱问题

这里主要讲述的是在并发请求下的数据逻辑处理的接口,如何保证数据的一致性和完整性,这里的并发多是大量用户发起的,也可能攻击者经过并发工具发起的并发请求


如例子:经过表设计防止并发致使数据错乱

  • 需求点 
    【签到功能】 一天一个用户只能签到一次,
    签到成功后用户获取到一个积分

  • 已知表 
    用户表,包含积分字段   
    高并发意淫分析(属于开发前的猜想): 
    在高并发的状况下,会致使,一个用户签到记录会有多条,或者用户签到后不止加一积分。

  • 个人设计 
    首先根据需求我会添加一张签到记录表,重点来了,这张表须要把用户惟一标识字段(ID,Token)和签到日期字段添加为惟一约束,或者惟一索引,这样就 能够防止并发的时候插入重复用户的签到记录。而后再程序代码逻辑里,先执行签到数据的添加(这里能够防止并发,添加成功后再进行积分的添加,这样就能够防 止重复的添加积分了。最后我仍是建议全部的数据操做都写在一个sql事务里面,  这样在添加失败,或者编辑用户积分失败的时候能够回滚数据。


如例子2(事务+经过更新锁 防止并发致使数据错乱 或者事物+Update的锁表机制)

  • 需求点: 
    【抽奖功能】 抽奖一次消耗一个积分 抽奖中奖后编辑剩余奖品总数 剩余奖品总数为0,或者用户积分为0的时候没法进行抽奖

  • 已知表:  
    用户表,包含积分字段 奖品表,包含奖品剩余数量字段

  • 高并发意淫分析(属于开发前的猜想): 
    在高并发的状况下,会致使用户参与抽奖的时候积分被扣除,而奖品实际上已经被抽完了

  • 个人设计: 
    在事物里,经过WITH (UPDLOCK) 锁住商品表,或者Update 表的奖品剩余数量和最后编辑时间字段,来把数据行锁住,而后进行用户积分的消耗,都完成后提交事物,失败就回滚。 这样就能够保证,只有可能存在一个操做在操做这件商品的数量,只有等到这个操做事物提交后,其余的操做这个商品行的事物才会继续执行。


如例子3(经过程序代码防止包并发下的数据错乱问题)

  • 需求点:
    【缓存数据到cache里】, 当缓存不存在的时候,从数据库中获取并保存在cache里,若是存在从cache里获取,天天10点必须更新一次,其余时间点缓存两个小时更新一次 到10点的时候,凡是打开页面的用户会自动刷新页面

  • 问题点:
    这里有个逻辑用户触发缓存的更新,用户刷新页面,当缓存存在的时候,会取到最后一次缓存更新时间,若是当前时间大于十点,而且最后缓存时间是10点前,则 会从数据库中从新获取数据保存到cache中。 还有客户端页面会在10点时候用js发起页面的刷新,就是由于有这样的逻辑,致使10点的时候有不少并发请求同时过来,而后就会致使不少的sql查询操 做,理想的逻辑是,只有一个请求会去数据库获取,其余都是从缓存中获取数据。(由于这个sql查询很耗服务器性能,因此致使在10点的时候,忽然间数据库 服务器压力暴增)

  • 解决问题:
    C#经过 (锁)lock,在从数据读取到缓存的那段代码前面加上锁,这样在并发的状况下只会有一个请求是从数据库里获取数据,其余都是从缓存中获取。


访问量大的数据统计接口

  • 需求: 用户行为数据统计接口,用来记录商品展现次数,用户经过点击图片,或者连接,或者其余方式进入到商品详情的行为次数

  • 问题点:
    这接口是给前端ajax使用,访问量会很大,一页面展现的时候就会有几十件商品的展现,滚动条滚到到页面显示商品的时候就会请求接口进行展现数据的统计,每次翻页又会加载几十件

  • 意淫分析:
    设想若是同时有1W个用户同时在线访问页面,一个次拉动滚动条屏幕页面展现10件商品,这样就会有10W个请求过来,服务端须要把请求数据入库。在实际线上环境可能还会超过这个请求量,若是不通过进行高并发设计处理,服务器分分钟给跪了。

  • 解决问题:
    咱们经过nodejs写了一个数据处理接口,把统计数据先存到redis的list里。(使用nodejs写接口的好处是,nodejs使用单线程异步事件机制,高并发处理能力强,不会由于数据逻辑处理问题致使服务器资源被占用而致使服务器宕机) 而后再使用nodejs写了一个脚本,脚本功能就是从redis里出列数据保存到mysql数据库中。这个脚本会一直运行,当redis没有数据须要同步到数据库中的时候,sleep,让在进行数据同步操做


高并发的下的服务器压力均衡,合理站点架设,DB部署

如下我所知道的:

  1. 服务器代理nginx,作服务器的均衡负载,把压力均衡到多台服务器

  2. 部署集群 mysql数据库, redis服务器,或者mongodb服务器,把一些经常使用的查询数据,而且不会常常的变化的数据保存到其余nosql    DB服务器中,来减小数据库服务器的压力,加快数据的响应速度。

  3. 数据缓存,Cache

  4. 在高并发接口的设计中能够使用具备高并发能力的编程语言去开发,如:nodejs 作web接口

  5. 服务器部署,图片服务器分离,静态文件走CDN

  6. DBA数据库的优化查询条件,索引优化

  7. 消息存储机制,将数据添加到信息队列中(redis list),而后再写工具去入库

  8. 脚本合理控制请求,如,防止用户重复点击致使的ajax多余的请求,等等。

并发测试神器推荐

  1. Apache JMeter

  2. Microsoft Web Application Stress Tool

  3. Visual Studio 性能负载

文章来源:SFLYQ

 

 

 转载2:http://www.cnblogs.com/lezai/p/4932396.html

以前我将高并发的解决方法误认为是线程或者是队列能够解决,由于高并发的时候是有不少用户在访问,致使出现系统数据不正确、丢失数据现象,因此想到 的是用队列解决,其实队列解决的方式也能够处理,好比咱们在竞拍商品、转发评论微博或者是秒杀商品等,同一时间访问量特别大,队列在此起到特别的做用,将 全部请求放入队列,以毫秒计时单位,有序的进行,从而不会出现数据丢失系统数据不正确的状况。

 

今天我通过查资料,高并发的解决方法有俩种,一种是使用缓存、另外一种是使用生成静态页面;还有就是从最基础的地方优化咱们写代码减小没必要要的资源浪费:(

1.不要频繁的new对象,对于在整个应用中只须要存在一个实例的类使用单例模式.对于String的链接操做,使用StringBuffer或者StringBuilder.对于utility类型的类经过静态方法来访问。

2. 避免使用错误的方式,如Exception能够控制方法推出,可是Exception要保留stacktrace消耗性能,除非必要不要使用 instanceof作条件判断,尽可能使用比的条件判断方式.使用JAVA中效率高的类,好比ArrayList比Vector性能好。)

首先缓存技术我一直没有使用过,我以为应该是在用户请求时将数据保存在缓存中,下次请求时会检测缓存中是否有数据存在,防止屡次请求服务器,致使服务器性能下降,严重致使服务器崩溃,这只是我本身的理解,详细的资料仍是须要在网上收集;

使用生成静态页面我想你们应该不模式,咱们见过不少网站当在请求的时候页面的后最已经变了,如“http://developer.51cto.com/art/201207/348766.htm”该页面实际上是一个服务器请求地址,在转换成htm后,访问速度将提高,由于静态页面不带有服务器组件;在这里我就多多介绍一下:

(一)什么是页面静态化:

简 单的说,咱们若是访问一个连接 ,服务器对应的模块会处理这个请求,转到对应的jsp界面,最后生成咱们想要看到的数据。这其中的缺点是显而易见的:由于每次请求服务器都会进行处理,如 果有太多的高并发请求,那么就会加剧应用服务器的压力,弄很差就把服务器 搞down 掉了。那么如何去避免呢?若是咱们把对 test.do 请求后的结果保存成一个 html 文件,而后每次用户都去访问 ,这样应用服务器的压力不就减小了?

那么静态页面从哪里来呢?总不能让咱们每一个页面都手动处理吧?这里就牵涉到咱们要讲解的内容了,静态页面生成方案… 咱们须要的是自动的生成静态页面,当用户访问 ,会自动生成 test.html ,而后显示给用户。

(二)下面咱们在简单介绍一下要想掌握页面静态化方案应该掌握的知识点:

一、 基础- URL Rewrite

什么是 URL Rewrite 呢 ? URL 重写。用一个简单的例子来讲明问题:输入网址 ,可是实际上访问的倒是 abc.com/test.action,那咱们就能够说 URL 被重写了。这项技术应用普遍,有许多开源的工具能够实现这个功能。

二、 基础- Servlet web.xml

若是你还不知道 web.xml 中一个请求和一个 servlet 是如何匹配到一块儿的,那么请搜索一下 servlet 的文档。这可不是乱说呀,有不少人就认为 /xyz/*.do 这样的匹配方式能有效。

若是你还不知道怎么编写一个 servlet ,那么请搜索一下如何编写 servlet.这可不是说笑呀,在各类集成工具漫天飞舞的今天,不少人都不会去从零编写一个 servlet了。

(三)基本的方案介绍

 
其中,对于 URL Rewriter的部分,能够使用收费或者开源的工具来实现,若是 url不是特别的复杂,能够考虑在 servlet 中实现,那么就是下面这个样子:

 

 
 
总 结:其实咱们在开发中都不多考虑这种问题,直接都是先将功能实现,当一个程序员在干到1到2年,就会感受光实现功能不是最主要的,安全性能、质量等等才是 一个开发人员最该关心的。今天我所说的是高并发,个人解决思路是,一、采用分布式应用设计二、分布式缓存数据库三、代码优化

 

 

 

10、工厂模式

 老师代码:

简单工厂模式实现加减乘除法

 

1.建立运算的接口

public interface Operation {
    //提供计算两个数字的方法
    double getResult(double num1,double num2);

}

2.建立对应的加减乘除四个实现类

复制代码
public class Addition implements Operation { //加法

    @Override
    public double getResult(double num1, double num2) {
        return num1+num2;
    }

}
复制代码
复制代码
public class Minus implements Operation { //减法

    @Override
    public double getResult(double num1, double num2) {
        return num1-num2;
    }

}
复制代码
复制代码
public class Multiplication implements Operation {//乘法

    @Override
    public double getResult(double num1, double num2) {
        return num1*num2;
    }

}
复制代码
复制代码
public class Division implements Operation {//除法

    @Override
    public double getResult(double num1, double num2) {
        return num1/num2;
    }
}
复制代码

3.建立工厂类

复制代码
public class OperationFactory {  //计算机的工厂类
    /*
     * 工厂模式 是咱们最经常使用的实例化对象的模式!
     * 用工厂的方法替代new!
     * 虽然代码量没有减小  可是 提升了程序的扩展性!
     */
    public static  Operation  getOperation(String o){
        Operation operation=null; //多态
        switch (o) {
        case "+":
            operation=new Addition();
            break;
        case "-":
            operation=new Minus();
            break;
        case "*":
            operation=new Multiplication();
            break;
        case "/":
            operation=new Division();
            break;
        }
        return  operation;
    }
}
复制代码

4.建立测试类 运行 测试结果

复制代码
public class FactoryTest {
    public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);
        System.out.println("请输入第一个数字:");
        double num1 = scanner.nextInt();
        System.out.println("请输入运算符:");
        String operation = scanner.next();
        System.out.println("请输入第二个数字:");
        double num2 = scanner.nextInt();
        //建立运算的实例对象
        Operation o = OperationFactory.getOperation(operation);
        //输出结果
        System.out.println(o.getResult(num1, num2));
    }
}

 

 

11、做业

一、soso项目

二、周五考试(机考+笔试),刷题刷题!

三、随时准备答辩soso项目

 

12、老师辛苦了!

相关文章
相关标签/搜索