Ordinary Road


  • 首页

  • 标签

  • 分类

  • 归档

【Java编程思想】八:复用类

发表于 2018-11-13 | 分类于 学习笔记 | 阅读次数:

一、组合语法

    代码复用是优化程序结构的一种必要手段,我们不需要重新编写代码,只需要获取一个类似的类在其基础上进行改动,保留一些原有的功能。改动的手段有组合跟继承两种。组合就是在一个新的类中引入原来的类的实例对象,这个对象是一个成员变量。如果不是一个对象,是一个基本数据类型,那么编译器会自动为其初始化。而当是一个对象的引用时,编译器则为其初始化为null,这样在使用这个对象时,就会发生错误。因此我们需要如下几个方法,初始化组合中的引用:

    1.在定义对象的地方,也就是对象引用建立的时候直接指定对象,这意味着对象的初始化发生在构造器调用之前。

    2.在类的构造器中,如第5章所述,我们可以通过构造器的形式,也就是new,初始化一个对象。

    3.就在这样使用这些对象之前,也就是所谓的延时加载,当我们必须要使用这个对象的时候,发现没有初始化该对象呢,那么进行初始化,这种方式可以提供系统性能,降低资源消耗,在必须用到的时候才创建。

    4.使用实例初始化,这种与构造器的相同之处都是在代码中显式的去创建一个对象,区别在于一个调用了构造器方法,一个赋值了一个实例。

下面分别举例说明四种初始化的方式:

    1.在定义对象的地方,我们在类中定义了String的引用,直接初始化赋值:

1
2
3
class Construct{
public String cont = "Hello World";
}

    2.在类的构造器中,如第五章的构造方法初始化:

1
2
3
4
5
6
7
8
9
10
package com.chenxyt.java.practice;
class Construct{
public String cont;
Construct(String cont){
this.cont = cont;
}
}
public class ConstructTest {
Construct construct = new Construct("Hello");
}

    3.延迟加载,我们需要用到这个对象引用了,判断一下是否为null,然后再决定是否初始化:

1
2
3
4
5
6
7
8
public class ConstructTest {
public static String s;
public static void main(String[] args) {
if(s==null){
s=new String("Hello");
}
}
}

    4.使用实例进行初始化,如下边这种,我们没有使用构造方法而是直接赋值了个实例进行初始化:

1
2
3
4
5
6
public class ConstructTest {
public static String s;
public static void main(String[] args) {
s="Hello";
}
}

二、继承语法

    另一种复用类的手段就是继承,我们创建一个新的类,如果没有显示的指明继承哪个类,那么它默认继承自Object类,我们也可以通过extends关键字继承一个指定的类。继承表现的是一种is-a的关系或者说是like-a,被继承的类称作父类或者基类,继承之后新生成的类称为子类或者派生类。子类拥有父类中所有被public或protected域修饰的方法、属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.chenxyt.java.practice;
class Father{
public String Name;
public int age;
private double money;
public void doPrint(){
System.out.println("Hello World");
}
}
public class Son extends Father {
public static void main(String[] args) {
Son son = new Son();
son.Name = "Zhang San";
son.doPrint();
//money是父类私有的域,所以子类不能进行访问
//son.money = 2014;
}
}

三、代理

    Java中还有一种基于组合与继承之间的关系,称作代理。但是实际上Java中并没有提供对它的直接支持。后续学习代理模式时会有专门的讲述。这里大概说一下,就是在使用组合的过程中,不提供成员对象的直接访问,而是通过一个新的方法暴露出来给其它使用者调用。避免直接暴露组合的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.chenxyt.java.practice;
class Construct{
public void print(){
System.out.println("print");
}
public void say(){
System.out.println("say");
}
public void clean(){
System.out.println("clean");
}
}
public class Son{
public Construct construct = new Construct();
public void delegation(){
construct.clean();
}
public static void main(String[] args) {
Son son = new Son();
son.delegation();
}
}

四、结合使用组合和继承

    同时使用组合和继承是很常用的事情,一般就是在一个子类中引用一个其它的类实例对象即可。诸如下边这种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;
class Father{
//---
}
class Mother{
//---
}
public class Son extends Father{
public Mother mother = new Mother();
public static void main(String[] args) {
//---
}
}

    关于继承还有一点说明,前文说到子类可以继承父类的全部,所以对于父类重载了的方法,子类也是可以继承并可以重载使用的。而且可以灵活的使用,避免了像C++中需要屏蔽父类该方法的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.chenxyt.java.practice;
class Father{
public void doFunc(){
//---
}
public void doFunc(String s){
//--
}
}
public class Son extends Father{
public void doFunc(int i){
//--
}
public static void main(String[] args) {
Son son = new Son();
son.doFunc();
son.doFunc("H");
son.doFunc(1);
}
}

五、组合和继承之间选择

    组合和继承都允许在新类中放置子对象,组合是显式的,而继承是隐式的。一般情况下,使用组合的地方多出是想使用其对象,就是在新类内部引用一个对象,然后利用这个对象完成一系列功能。而继承的使用场景主要是新类想使用原来类的一部分接口这种。在使用组合和继承的时候,要注意把我访问权限控制,因为一个完善的访问权限控制可以使程序更加健壮稳定。

六、protected关键字

    前文说到访问权限控制,在一般情况下,为了保证不被他人修改自己的私有域,父类的只有自己可以有权使用的域跟方法会被设置为private,而在一些特殊情况下或许我们会放宽这种约束,即我的东西其他人都不可以用,但是我的子类可以用,这时候就是前文所说的protected关键字的作用了。被protected关键字修饰的域,可以被子类访问:

1
2
3
4
5
6
7
8
9
10
package com.chenxyt.java.practice;
class Father{
protected String s = "Hello";
}
public class Son extends Father{
public static void main(String[] args) {
Son son = new Son();
System.out.println(son.s);
}
}

运行结果,因为字符串s被声明为protected,所以在子类的对象引用son可以访问到:

png1

七、向上转型

    在继承的关系中,由于子类继承了父类所有的方法,所以发给父类方法的所有消息,子类都可以接收,注意这里是发给父类方法的所有消息,而不是子类调用了继承的方法。编译器会知道传递的参数引用属于哪一种类型,并将其子类对象引用的类型转换成父类,这个过程称作向上引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.chenxyt.java.practice;
class Father{
private static String s;
Father(String s){
this.s = s;
}
public static void doFunc(Father father){
System.out.println(s);
}
}
public class Son extends Father{
Son(String s) {
super(s);
// TODO Auto-generated constructor stub
}

public static void main(String[] args) {
Son son = new Son("Hello");
Father.doFunc(son);
}
}

    如上所示,父类中有一个static的方法,并且方法的参数是父类的对象,在子类中直接通过类名.方法的形式调用了该方法,并传递了子类的对象引用作为参数。编译器没有报错,运行结果如下:

png2

    向上转型的概念来源于子类继承父类,子类在下,父类在上,由子类转换成父类,所以称作是向上转型。因为子类是父类的超集,所以向上转型总是安全的,相对也有向下转型,将在后文介绍。对于组合和继承,组合关系跟加方便且便于理解也常用,继承的关系虽然重要但不是说一定要多用。

八、Final关键字

    final关键字与static同等重要,final顾名思义就是最终的,不可改变的。所以Java中final关键字也是指“这是无法改变的”。一般无法改变主要是出于两种理由,设计或者效率,这两种相差比较远,所以容易误用。这里主要从三个方面说明final关键字的用法。

    1.final数据:当我们需要告诉编译器这一块数据区域不得改变的时候,比如一个固定的常量,或者一个在运行时候被初始化的值而我们却不希望它被改变,那么就可以使用final来修饰。比如类库中的某个成员变量,提供给开发者使用,但是不允许开发者修改这个变量在程序运行过程中的值,那么就可以使用final修饰。

1
2
3
4
5
6
7
package com.chenxyt.java.practice;
public class FinalTest{
private static final String ARG_NAME = "Hello World";
public static void main(String[] args) {
System.out.println(ARG_NAME);
}
}

如果一个常量被修饰为static final 那么表明这个常量是不可被修改且只有一份的,并且它的命名规则要使用大写且用下划线分割所有单词。对于基本数据类型,final表示这个值不可以修改,对于对象的引用,表示这个引用不可以修改,也就是不可以指向别的对象。

    2.final参数:Java中允许方法使用final修饰的参数,这意味着这个参数指向的对象不可以被修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.chenxyt.java.practice;
class Gzino{
public void doFunc(){
//--
}
}
public class FinalTest{
public void with(final Gzino g){
//error! 此处不可以修改该对象的引用
//g = new Gzino();
//可以使用该参数
g.doFunc();
}
public void without(Gzino g){
//没有被final修饰,可以使用
g = new Gzino();
g.doFunc();
}
public static void main(String[] args) {
FinalTest ft = new FinalTest();
ft.with(new Gzino());
ft.without(new Gzino());
}
}

    3.final方法:使用final方法的原因有两个,第一个是把它锁定,以防任何继承它的类修改这个方法。第二个原因是提高效率,如果一个方法被声明为final方法,那么编译器会跳过这个方法的常规调用方式(参数入栈,跳到方法的执行部分,然后返回清理栈中参数,最后返回结果),并且以方法体中的实际代码副本代替方法调用,这样减小了方法调用的开销,但在方法过大的时候却不实用,因此在JDK1.5之后,取消了该种形式的作用,final方法只用来锁定防止其它类修改。

    类中所有的private域都被隐式的注为private,因为不允许其它人修改。还有一种是final类,被final类修饰的方法我们认为这个类不能被其它人修改,也不能被继承,当你想达到这样的目的时,可以使用final类。

九、初始化及类加载

    在许多语言中,程序在启动的过程中发生了加载,然后是初始化,最后是程序运行。Java不同,Java采用不同的加载机制, 每个类的编译文件都在它自己的代码文件中,该文件只有在用到该程序的时候才被加载。所以一般可以说“类的代码在初次使用时才发生加载“,这通常是指加载发生在创建第一个类对象时,但是当static域被访问时,也会发生加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.chenxyt.java.practice;
class Insect{
private int i = 9;
protected int j;
public Insect() {
System.out.println("i=" + i + "--j=" + j);
j=39;
}
private static int x1 = printInit("static Insect.x1 initialized");
static int printInit(String s){
System.out.println(s);
return 53;
}
}
public class LoadTest extends Insect{
private int k = printInit("LoadTest.k initialized");
public LoadTest(){
System.out.println("k= " + k);
System.out.println("j= " + j);
}
private static int x2 = printInit("static LoadTest.x2 initialized");
public static void main(String[] args) {
System.out.println("LoadTest constructor");
LoadTest lt = new LoadTest();
}
}

运行结果:

png3

    分析上面的程序,程序在启动的时候先查找main函数,在多数语言中,main函数都是程序的入口,发现main方法在LoadTest类中,这时开始加载LoadTest类,在加载的过程中,发现这个类有基类,于是他继续加载基类,如果还有基类那么继续加载另一个基类,因为子类中可能会用到基类的内容。然后基类中的static域初始化,这时第一行打印。然后子类中的static域初始化,这时第二行执行。这时必要的类资源已经加载完毕,可以进行对象创建,第三行打印。初始化对象,首先对象中的所有基本类型被赋初始值0,对象被赋值为null,这是通过将对象内存设置为二进制零实现的。然后基类的构造函数被调用,这里是自动调用的,也可以通过super方法直接调用,这时第四行被打印。基类构造器和子类构造器执行相同的方式经过相同的过程。基类构造器执行完毕之后,实例变量按顺序被创建,最后执行子类的构造方法,所以最后两行被打印。

十、总结

    继承和组合都能从现有类中得到新的类型,区别在于组合一般是将现有类型作为底层实现的一部分使用,而继承复用的是接口。本章的重点内容是熟悉final关键字的用法以及类加载和初始化的过程,最后一点尤为重要。

【Java编程思想】七:访问权限控制

发表于 2018-11-13 | 分类于 学习笔记 | 阅读次数:

    一个优秀的程序员是通过不断的重构代码让自己的程序变得更加易用、可读和完善的。在重构修改的过程中,如果是一个类库编写人员,那么怎么样保证自己修改的部分不会影响到客户端编写人员(即使用这个类库的程序员)呢?同时也要避免他们对自己类库内部的程序进行改动。Java中提供了访问权限控制的概念,提供了三种不同级别的访问控制,访问开放程度由高到低依次为“public”、“protected”“private”,这样就能区分哪些内容是可用的,哪些内容是不可用的,从而将变动的事物与不变的事物区分开来。那么如何将所有的构建捆绑到一个内聚的类库单元中呢?Java提供了package加以控制,而访问权限控制的作用会因为类库是否在一个相同的package还是不同的package受到影响。

一、包:库单元

    包内包含一组类,它们在单一的名字空间下被组织在了一起。声明一个类所属的包使用package关键字,同时在另一个包中的类要访问其它包中的类使用import关键字导入要使用的包。这种方式可以在一定程度上避免重名的问题,因为包的名字要避免重名,而不同包内的类是可以根据具体的需求命相同的名字。包有效的将不同类的内容进行了隔离,同时也可以相互联系。
如下是com.chenxyt.java.test包中的Printer类,类中定义了一个print方法:

1
2
3
4
5
6
package com.chenxyt.java.test;
public class Printer {
public void print(String msg){
System.out.println(msg);
}
}

在另一个包com.chenxyt.java.practice中的PackageTest类,类中使用print方法:

1
2
3
4
5
6
7
8
package com.chenxyt.java.practice;
import com.chenxyt.java.test.Printer;
public class PackageTest{
public static void main(String[] args) {
Printer printer = new Printer();
printer.print("This is Package Test");
}
}

结果如下:

png1

看一下程序的目录结构:

png2

二、Java访问权限修饰词

    public:所有可见,被public修饰的内容在同一个包中的所有类都可见。同时Java提供默认的访问权限,即不被任何修饰符修饰的内容默认为public权限。

    private:私有可见,只有该类可见,该类的对象都不可见。如果一个类的构造函数被声明为private,那么就不能通过这个类的构造函数来进行初始化对象。

    protected:受保护的可见,与private不同,除了只有自己的类可见之外,该类的继承者也可见被修饰的域。除此之外还可以被当前包的类访问,但是其它包的类不可以访问,即便是使用了import的关键字

访问权限控制对程序结构控制的重要手段。

三、接口和实现

    这里没有详细的介绍Java中的接口跟实现,主要是基于访问权限控制来说的。一般的类开发者,为了方便他人使用,会在具体方法实现外部建立一层接口,只提供接口给外部开发人员调用,而不提供具体实现的方法。

四、类的访问权限

    在Java中,访问权限控制也可以确定包中的哪些类可以被访问,也就是说可以用来修饰类,一个文件中最多只能有一个使用public修饰的类。如果希望客户端程序员使用该类,并可以创建对象,那么就可以将该类修饰为public。并且被修饰为public的类必须要与该文件的名字完全相同

五、总结

    本章主要学习的是Java中的三种访问权限,熟练的掌握public、private和protected三种类型的概念以及应用场景将能更好的提高程序的健壮性和稳定性。

【Java编程思想】六:初始化与清理

发表于 2018-11-13 | 分类于 学习笔记 | 阅读次数:

一、用构造器确保初始化

    Java中通过提供构造器,确保每个类的对象都可以得到初始化,构造器的形式为:

1
2
3
className(){
//---
}

    在类的内部定义的一个与类名相同的方法,该方法没有返回值,没有返回值并不是返回void,而是真正的无返回值。该方法在对象创建时会自动执行。

1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
public class ConstructorTest{
ConstructorTest(){
System.out.println("Constructor Begin");
}
public static void main(String[] args) {
for(int i=0;i<5;i++){
ConstructorTest ct = new ConstructorTest();
}
}
}

运行结果如下:

png1

可以看见程序在初始化对象的时候自动执行了构造方法。

    上面是无参的构造方法,还可以显示的编写有参的构造方法,给类的成员变量赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;
public class ConstructorTest{
private int age;
ConstructorTest(int i){
age=i;
}
public static void main(String[] args) {
for(int i=0;i<5;i++){
ConstructorTest ct = new ConstructorTest(i);
System.out.println("Age=" + ct.age);
}
}
}

运行结果:

png2

    如果类中只有唯一的一个带参数的构造器,那么默认的无参构造器将不可用。

二、方法重载

    如果我们需求多种多样,既需要无参构造器,又需要有参构造器,我们可不可以把这两个方法都写出来呢?答案是肯定的,对于这种方法名相同,方法参数不同的写法,称作是重载。方法重载不仅支持构造器重载,也支持普通方法重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.chenxyt.java.practice;
public class ConstructorTest{
private int age;
private String name;
ConstructorTest(){

}
ConstructorTest(int i){
age=i;
}
ConstructorTest(String j){
name=j;
}
ConstructorTest(int i,String j){
age=i;
name=j;
}
public static void main(String[] args) {
ConstructorTest ct1 = new ConstructorTest();
ConstructorTest ct2 = new ConstructorTest(22);
ConstructorTest ct3 = new ConstructorTest("张三");
ConstructorTest ct4 = new ConstructorTest(23,"李四");
System.out.println("ct1 age=" + ct1.age + "---name=" + ct1.name);
System.out.println("ct2 age=" + ct2.age + "---name=" + ct2.name);
System.out.println("ct3 age=" + ct3.age + "---name=" + ct3.name);
System.out.println("ct4 age=" + ct4.age + "---name=" + ct4.name);
}
}

运行结果如下:

png3

    四个不同的构造函数初始化了四个不同的对象,可以从打印结果看出,没有初始化的值int类型为0,String类型为null。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;
public class ConstructorTest{
private static void print(int i){
System.out.println("int 类型打印" + i);
}
private static void print(String j){
System.out.println("String 类型打印" + j);
}
public static void main(String[] args) {
ConstructorTest.print(22);
ConstructorTest.print("张三");
}
}

    普通方法的重载最明显的使用就是println()方法,我们要保证打印任何内容都使用println()方法,而不是打印int类型用一个方法printint(),然后打印String类型用一个方法printString(),这时候方法重载是最好的办法。

png4

如上结果显示了两个重载方法的不同结果。

    重载方法中如果传入了低类型的参数,那么如果找不到合适的方法,会被隐式的提升成高类型的数据。如果传入了高类型的参数,如果不进行类型转换,那么编译器会报错。Java中区分重载方法的依据是参数类型和参数个数,参数顺序区分会造成程序易读性较差,在一些情况下,也可以使用返回值区分,当然前提是如果你关心方法的返回值。

三、缺省构造器

    如前文所述,默认的构造器,就是没有形参的构造器,作为一个类的缺省构造器。如果程序员没有显示的在代码中创建一个构造器,那么Java会自动帮你创建一个无参构造器来完成初始化。当然如果程序员显示的创建了构造函数,那么Java就不会给你创建缺省构造器了

四、this关键字

    this关键字也是Java中尤为重要的一个关键字,主要有三个作用:

    1.表示当前对象的引用,并返回当前对象,用法如下:

1
2
3
4
5
6
7
8
9
package com.chenxyt.java.practice;
public class ThisTest{
public ThisTest doFunc(){
return this;
}
public static void main(String[] args) {
ThisTest tt = new ThisTest().doFunc();
}
}

doFunc()方法返回的是一个ThisTest类型的值,所以此处使用return this,返回了该类型对象的引用。

    2.表示类的成员变量,在有的构造函数中,形参与成员变量使用同一个字符串,这时候可以用this来区分,带有this的表示成员变量,用法如下:

1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
public class ThisTest{
private String arg;
ThisTest(String arg){
this.arg = arg;
}
public static void main(String[] args) {
ThisTest tt = new ThisTest("嘻嘻");
System.out.println(tt.arg);
}
}

运行结果:

png5

此处this.arg表明了这个变量是成员变量,与构造方法的形参做了区分。

    3.可以在构造器中调用另一个构造器,使用this带参数代替构造器的方法名,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;
public class ThisTest{
ThisTest(int i,String j){
this(j);
System.out.println("我是构造器1");
}
ThisTest(String arg){
System.out.println("我是构造器2");
}
public static void main(String[] args) {
ThisTest tt = new ThisTest(2,"嘻嘻");
}
}

运行结果如下:

png6

    可以看到对象在初始化的时候使用了两个构造器,其中一个构造器是在另一个构造器中通过this()的方式进行了调用。

五、清理:终结处理和垃圾回收

    Java中的内存清理使用的是Java自带的垃圾回收机制,但是不管怎么来说,这种方式都不是绝对安全的。Java的垃圾回收机制清理的是通过new创建的对象,而在某些特殊情况下,可能有些对象不是通过new创建的,这些对象如果不使用的时候,垃圾回收器是不能准确的清理他们的从而造成了这块特殊的内存区域一直得不到释放。Java中提供了finalize()方法来处理这一部分特殊的内存区域。它的处理流程是这样的,在Java垃圾回收器启用之前,会先调用这个方法进行一些必要的回收操作。但是针对这一块特殊的区域,或者是new创建的需要被回收的对象,一般情况下只有当Java虚拟机内存快要消耗殆尽的时候,垃圾回收器才会启动,毕竟启动垃圾回收器也是需要消耗资源的,所以不可能说实时存在。所说的特殊的创建对象方式,一般是指“本地方法”使用时,也就是在Java中调用非Java代码的时候发生的。所以一般情况下是不需要使用finalize()方法的。

    finalize()通常还有另一个用法,由于它是在Java垃圾回收器启动之前执行的,所以可以用它来判断终结状态,即判断一个对象是否满足回收条件。

六、成员初始化

    Java尽量保证每个变量在使用之前都进行了初始化操作,变量分为局部变量和成员变量,局部变量如果没有显示的初始化,在使用它的时候会报错,而成员变量不会,如果没有显示的初始化一个成员变量,那么它会被默认的分配一个指定的值。

七、构造器初始化

    如前边对构造器的阐述,可以使用构造器来初始化类的成员变量,当对象被实例化之后,对象的成员变量会被初始化。静态成员变量只有在第一次使用它是才会被初始化,后边再次用到时不会被初始化。初始化顺序为创建对象时,先初始化这个类的静态变量,然后在堆上为这个对象分配内存,最后执行构造函数。

八、数组初始化

    数据是一系列相同数据类型封装起来的序列,它的初始化可以发生在任何时候,int[] a1表示一个int类型的数组,这个数组内部所有的值都是int类型,a1只是这个数组的一个引用,可以显示的通过如int[] a1={1,2,3};的形式进行初始化。如果不能确定数组的内容或者是长度,则可以通过new的形式来创建一个数组。int[] a = new int[20];这种创建也只是创建了一个引用数组,直到数组中的每一个字段都有确切的值了,初始化才真正的完成。如a[1]=3;

    使用数组我们可以构建一个变参的函数,就是当方法的参数类型和个数都不确定的时候,我们可以使用一个数组作为形参。因为Object类是所有类型的父类,所以这个形参数组的类型就是Obejct类,对于基本数据类型,因为都有对应的包装类,所以也可以转换成Obejct类进行使用。

1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
public class DifArgTest{
public static void printArray(Object[] args){
for(Object obj:args){
System.out.print(obj + "");
}
}
public static void main(String[] args) {
DifArgTest.printArray(new Object[]{"我今年",new Integer(24),"岁!"});
}
}

运行结果:

png7

    可以看到printArray()方法将Obejct的数组内容都打印了出来。但是这样传参数是要写成数组的形式,未免有些臃肿。JDK1.5之后真正的变参函数出来啦。

1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
public class DifArgTest{
public static void printArray(Object...args){
for(Object obj:args){
System.out.print(obj + "");
}
}
public static void main(String[] args) {
DifArgTest.printArray("我","今年",24,"岁");
}
}

    用“…”代替了原有的数组符号,同时传递的参数也变得更加简洁了。运行结果:

png8

九、枚举类型

    这里只是初步的了解一下枚举类型,enum,Java中的enum要比C++更加完备。以下是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
package com.chenxyt.java.practice;
public class EnumTest{
public enum EnumSet{
FIRST,SECOND,THIRD
}
public static void main(String[] args) {
EnumSet es = EnumSet.FIRST;
System.out.println(es);
}
}

    创建一个枚举类型,类型内部的实例值是常量,因此按照通用的命名规范进行大写。同时需要使用枚举时,可以初始化一个引用然后进行赋值。结果如下:

png9

    当我们创建枚举的时候编译器会自动为我们添加一些有用的特性,我觉得这是与其它语言相比更加完备的地方,比如它会创建一个toString()方法,这也就是为什么我们上边可以使用syso打印出来。编译器还会创建ordinal()方法,用来表示特定enum常量的声明顺序,以及一个static values()方法,该方法是一个静态的方法可以通过enum名字进行访问,方法的作用是按照enmu的声明顺序,产生一个由enum常量值组成的数组。

1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
public class EnumTest{
public enum EnumSet{
FIRST,SECOND,THIRD
}
public static void main(String[] args) {
for(EnumSet et:EnumSet.values()){
System.out.println("value is:" + et + "---ordinal is:" + et.ordinal());
}
}
}

运行结果如下:

png10

    enum看起来像是一种新的数据类型,但是实际上enum是一个类,并且具有自己的方法。

    除了上边的特性之外,enum还有个更加实用的特性,由于它是一个常量集,因此可以和switch语句完美匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.chenxyt.java.practice;
public class EnumTest{
public EnumSet es;
EnumTest(EnumSet es){
this.es = es;
}
public enum EnumSet{
FIRST,SECOND,THIRD
}
public void doSwitchPrint(){
switch(es){
case FIRST:
System.out.println("This is First");
break;
case SECOND:
System.out.println("This is Second");
break;
case THIRD:
System.out.println("This is Third");
break;
default:
System.out.println("This is Error");
}
}
public static void main(String[] args) {
EnumTest et1 = new EnumTest(EnumSet.FIRST);
EnumTest et2 = new EnumTest(EnumSet.SECOND);
EnumTest et3 = new EnumTest(EnumSet.THIRD);
et1.doSwitchPrint();
et2.doSwitchPrint();
et3.doSwitchPrint();
}
}

运行结果如下:

png11

    以上是对enum的一个初步了解,后续将会单独阐述。

十、总结

    本章在Java中占有了至关重要的作用,也可以说初始化比较重要。总的来说要掌握如何进行初始化,方法的重载,重载不仅发生在构造方法中,也可以发生在普通方法。this关键的使用可以说很重要,但是便于理解。垃圾回收以及枚举只是初步的了解了一下,后续还会单独进行学习。

【Java编程思想】五:控制执行流程

发表于 2018-11-13 | 分类于 学习笔记 | 阅读次数:

一、true和false

    关系操作符构造的条件语句如“==”的返回值是true和false,Java中不允许将一个数字作为布尔值使用。

二、if-else

    if else语句与其它语言的相同,其中else是可选的。if else用来实现多种条件下的执行。

三、迭代

    while、do-while、for用来控制循环,有时候将他们称为迭代语句。语句会重复执行,直到起控制作用的布尔值得到“假”的结果时停止

四、for-each

    for-each是一种更加简洁的for语句,语法如下:

1
2
3
for(int i:x){
//---
}

    x是要被访问的循环体,i是一个变量,类型int只是一种,具体的类型要与x内部的值的类型相同,这个语句的意思就是循环取x内部的值赋值给i

五、return

    return关键字有两个用途,一方面指定一个方法返回什么值,另一方面它会强制结束当前方法,并返回那个值。

六、break和continue

    在任何迭代语句的主体部分,都可以用break和continue控制循环的流程,其中break用于强制退出循环,不执行循环剩余的语句,比如一共五组数据循环到第三组break,那么后面两组不管了继续执行下边的数据。continue是停止当前的迭代,退到循环开始执行下一次迭代,比如一共五组数据执行到第三组开始的时候continue,那么这个循环体主体剩余的部分不执行,继续从第四组开始执行。

七、臭名昭著的go-to

    go-to语句会破坏代码的逻辑结构,降低代码的可读性。因此不建议使用。

八、switch

    switch也被划为一种选择语句,根据整数表达式的值,从一系列语句中选择一组执行。语法如下:

1
2
3
4
5
6
7
8
9
10
switch(a){
case value1:
//---;
break;
case value2:
//---;
break;
default:
//---;
}

九、总结

    文中多数控制语句在其它语言中都通用,注意for-each语句的使用。

【Java编程思想】四:操作符

发表于 2018-11-12 | 分类于 学习笔记 | 阅读次数:

一、更简单的打印语句

    在第二章中使用了一个打印语句:

1
System.out.println("Hello World");

    结合static的静态包用法,如果这个语句在多个地方进行调用,那么可以将这个语句写成静态方法,然后通过导入静态包的形式使用静态方法:

test包下的Printer类:

1
2
3
4
5
6
package com.chenxyt.java.test;
public class Printer {
public static void print(String msg){
System.out.println(msg);
}
}

practice包下的TestStatic类:

1
2
3
4
5
6
7
package com.chenxyt.java.practice;
import static com.chenxyt.java.test.Printer.*;
public class TestStatic{
public static void main(String[] args) {
print("This is TestStatic");
}
}

二、使用Java操作符

    Java与其他语言一样,支持加号,正号,减号,负号,乘除号等操作符,同时“=”,“==”和“!=”不光可以操作基本操作类型,还可以操作所有的对象。此外,String类支持“+”和“+=”。

三、优先级

    当一个表达式中存在多个操作符时,操作符的优先级就尤为重要了,它决定了程序运算操作执行的先后顺序。Java中的计算顺序与其它语言的基本相同,先计算乘除,再计算加减,有括号的先计算括号里边的。System.out.println()语句中包含“+”的操作符号,简单的只是进行字符串的连接,复杂一点的就是当编译器发现“+”前边是一个String类型,会尝试将“+”后面的内容转换成String类型。

四、赋值

    赋值操作使用的是“=”操作符,将“=”右边的值赋给左边,右值可以是任意的常数、变量或者是表达式,只有它能生成一个值即可,而左值必须是一个明确的已命名的变量,就是必须要有个物理空间来进行存储。比如,可以将一个常数赋给变量:

1
a=4;

但是却不能将一个变量赋值给一个常数:

1
4=a;

    对于基本的数据类型,赋值操作没有引用的涉及,只是单纯的将一个值赋值给另一个值。例如a=b,当b再次被修改时,a不会受到影响,因为a与b相互独立。但是对于对对象的赋值来说,情况却大大不同,因为我们对对象的操作是操作了对象的引用,所以当一个对象赋值给另一个对象时,实际上是拷贝了引用到左值,也就是比如c和d是指向两个不同对象的引用,当c=d时,实际发生的情况是c和d都指向了原本只有d指向的对象。而c被赋值之后,原来的引用丢失了,它曾经所指向的不再被引用的对象被垃圾回收器回收了。如下例子创建了两个不同的对象,进行赋值操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.chenxyt.java.practice;
class Tank{
int level;
}
public class CopyTest{
public static void main(String[] args) {
Tank tk1 = new Tank();
Tank tk2 = new Tank();
tk1.level = 2;
tk2.level = 3;
System.out.println("tk1.level = " + tk1.level + "---tk2.level = " + tk2.level);
tk1=tk2;
System.out.println("tk1.level = " + tk1.level + "---tk2.level = " + tk2.level);
tk2.level=5;
System.out.println("tk1.level = " + tk1.level + "---tk2.level = " + tk2.level);
}
}

运行结果如下:

png1

    tk1和tk2分别是两个独立的对象,它们内部有level属性,赋值之前两个对象的属性值不同,赋值操作完成之后两个对象的属性值相同,当再次更改对象tk2的level值时,预期的理想情况是不会影响tk1的值,但实际结果并非如此,tk1与tk2对象的属性相同,这与前面的分析结果相同。
    Java中这种针对对象的特殊现象叫做“别名现象”,如果想避免这种现象的话,可以使用如下操作,赋值操作针对属性而不是对象的引用:

1
tk1.level=tk2.level

这样操作就可以保持两个对象本身相互独立。

    在调用方法传参的时候也是会产生“别名问题”,如下例子,调用方法copy之后,理想的是只改变了方法内的值,实际上是改变了方法之外对象的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.chenxyt.java.practice;
class Tank{
int level;
}
public class CopyTest{
public static void copy(Tank tk3){
tk3.level = 9;
}
public static void main(String[] args) {
Tank tk1 = new Tank();
tk1.level = 2;
System.out.println("tk1.level = " + tk1.level);
copy(tk1);
System.out.println("tk1.level = " + tk1.level);
}
}

运行结果,方法外部tk1对象的值被修改了

png2

五、算术操作符

    Java中的算术操作符与其它语言基本类似,有加号(+)、减号(-)、乘号(*)、取整(/)、取余(%),同时也具有简化运算符的功能如要将x加4之后再赋值给x,则可以写成:

1
x+=4;

六、自动递增递减

    Java中的递增递减操作与其它语言基本类似,递增符合(++),递减符合(–),操作目的是快速使一个整数加1或者减1如a++等同于a=a+1,这两种符号分别有两种使用方式称为“前缀式”和“后缀式”,前缀式意味着符号在变量前边,后缀式意味着符号在变量后边。二者的区别,对于前缀式是先做运算再取值,如a=1,b=++a,此时a跟b的值都是2,而后缀式则是先取值再做运算,如a=1,b=a++,此时b的值为1,a的值为2

七、关系操作符

    关系操作符生成的是一个boolean(布尔)结果,它们计算操作数之间的关系,如果关系为真则结果为true,如果关系为假则结果为false。关系操作符号包括小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、等于(==)以及不等于(!=)。等于和不等于适用于所有的基本数据类型,而其它的操作符不适用于boolean类型,因为它们的值为true或者false,比较大小没有意义。
操作符“==”和“!=”同样适用于操作对象,但是与基本操作类型相比有一些不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;
public class OperationTest{
public static void main(String[] args) {
Integer itg1 = new Integer(22);
Integer itg2 = new Integer(22);
System.out.println(itg1==itg2);
System.out.println(itg1!=itg2);
int int1 = 22;
int int2 = 22;
System.out.println(int1==int2);
System.out.println(int1!=int2);
}
}

如上创建了两个Integer对象和两个int基本数据类型值,分别做“==”和“!=”判断,结果如下:

png3

    从结果可见,两个对象是“!=”,而两个int基本数据类型值是“==”,这是为什么呢?因为对于对象来说,“==”和“!=”比较的是对象的引用,虽然这两个对象的值相同,但是他们对象的引用并不是一个,也就是他们在内存中有两个不同的存储位置,所以不相同。所以要想比较对象的值是否相同,我们可以使用对象的equals()方法来进行比较,它是Object类的一个通用方法,第一章中说到过所有类的父类都是Object,因此任何类的对象都可以调用这个方法。

1
2
3
4
5
6
7
8
package com.chenxyt.java.practice;
public class OperationTest{
public static void main(String[] args) {
Integer itg1 = new Integer(22);
Integer itg2 = new Integer(22);
System.out.println(itg1.equals(itg2));
}
}

结果如下:

png4

    新的问题来了,我们看如下示例,我们自定义了一个新的类,创建了两个对象进行比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;
class Oper{
int i;
}
public class OperationTest{
public static void main(String[] args) {
Oper op1 = new Oper();
Oper op2 = new Oper();
op1.i=2;
op2.i=2;
System.out.println(op1.equals(op2));
}
}

结果如下:

png5

    结果仍然为fasle!这是为什么呢?这是因为Object类中equals()方法实际上是比较两个引用是否相同,就是它与“==”的本质效果是一样的,但是在第一个示例中的Integer类中,Java覆盖了这个方法,方法内容判断两个对象的类型是否相同以及值是否相同即可。而我们自己创建的Oper类并没有覆盖这个方法,所以沿用的还是Object类的方法。常见的String类也是覆盖了equals()方法,效果与Integer类的对象相同。

八、逻辑操作符

    Java中的逻辑操作符与其它语言基本类似,包括与(&&)、或(||)、非(!),不同的是Java中的逻辑操作符只能应用与布尔值之间或者是表达式结果为布尔值。

1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
public class OperationTest{
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = 3;
System.out.println((a>b)&&(b>c));
System.out.println((a>b)||(b<c));
System.out.println(!(a<b));
}
}

运行结果如下:

png6

    逻辑运算满足如下关系,“&&”逻辑与运算符必须两边同时为真则结果为真,否则为假。“||”逻辑或运算符只要有一边为真则结果为真,“!”非运算符取对应值的对立面,即非真则假。对于逻辑与和逻辑或运算还有个名词叫做短路,即当两个算式做逻辑与运算时,如果左边第一个算式为假,则显然这个逻辑表达式最后的结果一定为假,那么就没有必要进行下边的表达式计算,直接结束运算,这个过程称作短路。

九、直接常量

    Java中同样可以使用例如“π”这种常量表示。

十、按位操作符

    Java中的按位操作符与其他语言基本相似,有按位与(&)、按位或(|)、按位异或(^)和按位非(~)操作符。它们针对基本数据类型的一个比特位(bit)进行运算。运算规则如下,&操作符必须同时为1才为1,|操作符只有同时为0时才为0,^操作符只要有一个为1就为1,~是单目运算符,取反,若值为1则结果为0,反之为1。按位操作符除了~还可以与“=”合起来使用,如&=或者|=、^=。
对于布尔值,同样可以进行按位与、按位或和按位异或运算,但是不能进行按位非运算,并且他们不会被短路,不管第一个表达式结果是什么,都会继续运算下去。

十一、移位操作符

    移位操作符操作的也是二进制的“位”,移位操作符只能用来处理整数类型。移位操作也是二元操作符,一共有三种,左移操作符(<<),右移操作符(>>),无符号右移操作符(>>>),左移操作符是操作符左边的数向左移动操作符右边指定的位数,低位补0,右移操作符是操作符左边的数向右移动操作符右边指定的位数,其中正数高位补0,负数高位补1。无符号右移操作符是Java独有的一种,即不管正数还是负数,右移之后高位都补0。关于移位操作有两点说明,一是高位指的是左边的位,二是对于int类型,最大长度为32位,对于long类型最大长度为64位。移位示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.chenxyt.java.practice;
public class OperationTest{
public static void main(String[] args) {
int a = 6297;
int b = -6297;
System.out.println(Integer.toBinaryString(a));
System.out.println(Integer.toBinaryString(b));
System.out.println(Integer.toBinaryString(a>>5));
System.out.println(Integer.toBinaryString(a<<5));
System.out.println(Integer.toBinaryString(a>>>5));
System.out.println(Integer.toBinaryString(b>>5));
System.out.println(Integer.toBinaryString(b<<5));
System.out.println(Integer.toBinaryString(b>>>5));
}
}

    我们以int类型6297为例,分别对正6297和负6297做三种位移操作,结果如下:

png7

满足前边所说,左移低位补0,右移正数高位补0,低位补1,因为高位0没有实际意义,所以没有写出。同时,因为数据类型为int类型,所以最大长度为32位,超出的部分被截断了。

    如果对char、short、byte类型的数值进行移位处理,那么他们会被先转成int类型,并且得到的结果也只是int类型。只有数值右端对的低5位才会有用,这样可以防止移位得到超过int类型的最大位数。比如说5<<34,因为最多能移32位,所以要把34转成二进制,然后取低5位,34转换成2进制是100010,取低5位就是00010,也就是5<<34实际上就是5<<2=10100=20。同样的对于long类型,long的最大长度为64位,所以数值的低6位有效。移位运算符同时也支持“<<=”等运算操作。

十二、三元操作符IF-ELSE

    三元操作符也称条件操作符,它有三个操作符。表达形式如下:
boolean-exp?value1:value2;
boolean-exp是一个布尔表达值,如果值为真,则返回value1的值,如果值为假,则返回value2的值。如:

1
2
3
4
5
6
7
8
package com.chenxyt.java.practice;
public class OperationTest{
public static void main(String[] args) {
int a = 5;
int b = a>10?a*100:a*10;
System.out.println("b===="+ b);
}
}

运行结果:

png8

因为a<10,所以布尔表达式的值为假,所以取value2也就是a*10的值。

十三、字符串操作符+和+=

    Java中可以使用+和+=连接String类型,并且当操作符左边你的数为String时,Java会试图将操作符右边的类型转换成String。

十四、使用操作符常犯的错误

    注意“=”和“==”的区别,以及逻辑运算符如(&&)和位运算符(&)的区别和前自增“++i”和后自增“i++”的区别。注意“==”和“equals()”的使用,注意别名现象。

十五、类型转换操作符

    Java中有跟其它语言相同的转换方式,(类型)值形式,也可以使用基本类型的包装器的转换方法进行转换。

十六、Java没有size of

    C语言中使用size of来获取程序占用的内存字节大小,目的是确定平台的移植操作,Java中没有这个方法,也就是不需要获取这个值,因为Java在不同的平台下基本数据类型具有相同的大小,可以便捷的移植。

十七、总结

    操作符在各个语言中基本通用,熟练掌握自增自减、逻辑运算、位运算以及移位操作即可,理解“==”和“equals()”的原理,理解别名现象。

【Java编程思想】static关键字的四种用法

发表于 2018-11-09 | 分类于 学习笔记 | 阅读次数:

    上一章说到了static关键字,static是Java中很重要的一个关键字,在一些场景下可以达到优化程序的效果。本文学习它的不同使用场景。在此之前先了解一下变量的类型。Java中变量分为两种,按作用域分为成员变量和局部变量。成员变量是在类中声明的,不属于任何方法,当前类中有效。局部变量是声明在方法中的,出了当前方法即超出作用域。接下来正文说一下static关键字的四中使用场景:

一、修饰成员变量

    static最常用的作用就是修饰成员变量,被static修饰的成员变量也叫做类变量。它与普通成员变量的区别是,它在内存中只有一份拷贝,Java虚拟机只为其分配一次内存。通俗的讲就是,这个变量,不管被多少人使用,他们使用的都是同一个变量,彼此的修改会有影响。可以通过类名.变量的形式进行访问。而普通的成员变量则是没实例化一个对象就产生一个拷贝,虚拟机为每一个变量拷贝都分配了内存。所以类变量的用处一般在于进行变量共享的时候。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestStatic {
private String name;
private int age;
public void print(){
System.out.println("Name:" + name + "---Age:" + age);
}
public static void main(String[] args) {
TestStatic ts1 = new TestStatic();
TestStatic ts2 = new TestStatic();
ts1.name = "zhangsan";
ts1.age = 22;
ts2.name = "lisi";
ts2.age = 33;
ts1.print();
ts2.print();
}
}

这是普通的成员变量的例子,ts1和ts2对于变量name和age分别拥有自己的副本,互相不影响,所以赋值之后,他们能得到他们期望的值。

png1

接下来将成员变量age修改为类变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestStatic {
private String name;
private static int age;
public void print(){
System.out.println("Name:" + name + "---Age:" + age);
}
public static void main(String[] args) {
TestStatic ts1 = new TestStatic();
TestStatic ts2 = new TestStatic();
ts1.name = "zhangsan";
ts1.age = 22;
ts2.name = "lisi";
ts2.age = 33;
ts1.print();
ts2.print();
}
}

这时由于age变成了类变量,因此ts1和ts2共用了一个变量副本,先赋值的会被后赋值的覆盖掉。

png2

此外第二个示例代码中,静态成员变量使用了对象.变量的方式进行调用,这里编译器会给出警告,使用类名.方法之后警告就会解除。

png3

二、修饰成员方法

    static另一个作用是修饰类中的方法,其目的是可以通过类名.方法的形式调用方法,而避免频繁的创建对象。同时对于存储空间来说也只有一个,不同对象调用的是同一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestStatic {
private String name;
private static int age;
public static void print(){
System.out.println("Name:" + name + "---Age:" + age);
}
public static void main(String[] args) {
TestStatic ts1 = new TestStatic();
TestStatic ts2 = new TestStatic();
ts1.name = "zhangsan";
ts1.age = 22;
ts2.name = "lisi";
TestStatic.age = 33;
TestStatic.print();
TestStatic.print();
}
}

将之前代码中的print方法改为使用static修饰之后,这个方法就可以不用对象.方法的形式调用了。

三、修饰代码块

    我们先看一下对象的初始化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Load {
public Load(String msg){
System.out.println(msg);
}
}
public class TestStatic{
Load ld1 = new Load("普通变量1");
Load ld2 = new Load("普通变量2");
static Load ld3 = new Load("静态变量3");
static Load ld4 = new Load("静态变量4");
public TestStatic(String msg){
System.out.println(msg);
}
public static void main(String[] args) {
TestStatic ts = new TestStatic("TestStatic 初始化");
}
}

    在类TestStatic中,我们初始化了两个普通成员变量和两个静态成员变量,并在main函数开始的时候初始化了TestStatic对象。结果如下:

png4

    静态成员变量最先被初始化,并且按照执行的先后顺序进行初始化。其次初始化的是成员变量,最后初始化的是构造方法。所以在创建一个对象的时候,最先被初始化的是静态成员变量。

    在看另外一个调用了静态方法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Load {
public Load(String msg){
System.out.println(msg);
}
}
public class TestStatic{
Load ld1 = new Load("普通变量1");
Load ld2 = new Load("普通变量2");
static Load ld3 = new Load("静态变量3");
static Load ld4 = new Load("静态变量4");
public TestStatic(String msg){
System.out.println(msg);
}
public static void staticFunc(){
System.out.println("静态方法");
}
public static void main(String[] args) {
TestStatic.staticFunc();
System.out.println("@@@@@@@@@");
TestStatic ts = new TestStatic("TestStatic 初始化");
}
}

我们在创建对象之前先调用了静态方法,结果如下:

png5

    我们可以看到,静态成员的初始化发生在创建对象之前,确切的说是在调用静态方法之前就已经被初始化了。并且,当我们创建对象的时候,原本被初始化过的静态成员变量跟静态方法没有再次被初始化。

    这时我们的static的作用就是,修饰一段都需要被修饰为static的域。被static修饰的代码域,域中所有的内容都被当成static变量,且优先初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Load {
public Load(String msg){
System.out.println(msg);
}
}
public class TestStatic{
Load ld1 = new Load("普通变量1");
Load ld2 = new Load("普通变量2");
static Load ld3;
static Load ld4;
static{
ld3 = new Load("静态变量3");
ld4 = new Load("静态变量4");
}

public TestStatic(String msg){
System.out.println(msg);
}
public static void staticFunc(){
System.out.println("静态方法");
}
public static void main(String[] args) {
TestStatic.staticFunc();
System.out.println("@@@@@@@@@");
TestStatic ts = new TestStatic("TestStatic 初始化");
}
}

修改之前的代码将静态成员变量放在由static修饰的域中,结果如下:

png6

与分开修饰结果相同。

四、静态导入

    前边三种都是比较常用的场景,还有一种不是太常用,是JDK1.5之后新加入的功能,导入一个带有静态方法的包,并将包修饰为static,从而在当前类中直接调用修饰的静态方法,就好像是自己的方法一样:

1
2
3
4
5
6
package com.chenxyt.java.test;
public class Printer {
public static void print(String msg){
System.out.println(msg);
}
}

然后另一个包中使用import static导入这个类

1
2
3
4
5
6
7
package com.chenxyt.java.practice;
import static com.chenxyt.java.test.Printer.*;
public class TestStatic{
public static void main(String[] args) {
print("This is TestStatic");
}
}

运行结果:

png7

五、总结

    static是Java语言中的一个很重要的关键字,主要用途有三个方面修饰成员变量,修饰成员方法以及修饰代码块。使用static修饰的成员变量在类加载的时候就已经被初始化了,它属于类变量,不属于某个对象,所有该类的实例化对象拥有同一个静态成员变量副本,常用的用途可以用它来做计数器。

【Java编程思想】一切都是对象

发表于 2018-11-09 | 分类于 学习笔记 | 阅读次数:

一、用引用操作对象

    在Java中,一切都是对象,但是操作对象的标识符是对象的一个“引用”,这一关系可以理解为电视机与遥控器的关系。我们通过遥控器控制电视机,我们在房间里不管在哪都可以使用遥控器操作电视机。同时,遥控器又可以独立存在,即引用可以独立存在,有如下代码:

1
String s;

    上一行代码仅仅是创建了一个引用,而并非是一个对象。如果此时我们向s发送消息的话,将报出运行时异常的错误。因为前文讲到Java中消息的传递是建立在对象之间来完成的。因此我们需要在建立引用的时候对其进行初始化,使其指向一个对象:

1
String s = "abc";

    这里使用了Java语言的特性,字符串可以使用引号引起来用来进行初始化操作。

二、必须由你创建所有的对象

    我们创建了一个操作对象的引用,通常希望它与一个对象相关联,以便我们可以进行消息的传递,完成业务功能。Java语言中使用new关键字来完成这个操作,它的含义是“给我创建一个对象与我的引用关联”。所以上述代码可以改为:

1
String s = new String(abc");

    它表示为给我一个String类型的对象,并且初始化为“abc”。

    当我们创建好对象时,他们是怎么样进行内存分配的呢?程序在计算机中有如下五个地方可以分配内存:

    1.寄存器:这是最快的存储区,他在处理器的内部,并且数量非常有限。因此它是根据需求进行分配,你基本不能自己控制他的分配。

    2.堆栈:位于通用(RAM)随机存储器中,通过堆栈指针的移动进行内存分配,速度仅次于寄存器。在堆栈中分配内存空间的项,Java系统必须明确知道其声明周期,以便更好地进行指针的移动。这也从一定程度上进行了限制,因此Java中,对象的引用存在堆栈中,但是对象并不存在堆栈中。

    3.堆:一种同样存放在RAM的内存池,存放Java中所有的对象,且不需要知道其声明周期。是Java程序中主要的内存分配区域,只需要new关键字即可获得内存,但这种效率要比在堆栈上低很多。

    4.常量存储:常量池通常存在程序代码内部,这样使得他们永远不会被改变。在部分嵌入式系统中,常量本身会与其它部分分离,这种情况通常将常量存在(ROM)只读存储器中。

    5.非RAM存储:数据可以完全脱离程序存储在外部,从而不受程序的控制。比如文件流对象和持久化对象,文件流通常发给另外一台机器,而持久化对象则通常是存储在硬盘中。

    Java中有一种特殊的类型称作是基本类型,通常情况下,new将对象存储在堆里,当创建一个简单的小的对象时,显得不是很有效,因此Java采用跟C或者C++相同的方式,不用new创建变量,而创建一个并非是引用的自动变量,这个变量直接存储值,并将其存储在堆栈中。与其他语言不同的是,Java中所有的基本类型的大小在各个平台都相同,这使得Java程序有更好的跨平台移植性。

png1

    所有的数值类型都有正负号,所以Java中不存在无符号类型。基本类型具有包装器类,使得可以在堆中创建一个非基本对象,用来表示对应的基本类型。例如:

1
2
char c = ‘x’;
Character ch = new Character(c);

也可以这样用:

1
Character ch = new Character(‘x’);

Java SE5的自动包装功能可以自动的将基本类型转换为包装器类型:

1
Character ch = ‘x’;

也可以反向转换:

1
char c = ch;

    Java中还提供了两个用于高精度计算的类:BigInteger和BigDecimal,他们大体属于“包装器类”,但是他们没有对应的基本类型,不过可以通过方法调用的方式与int 和 float进行交互操作。BigInteger支持任意精度的整数,BigDecimal支持任意精度的浮点数。

    Java与其他语言一样都提供了数组的功能,不同的是,Java语言保证了数组的安全性,会确保数组被初始化才使用,不会出现其他语言中的访问没有被初始化的内存区域。当创建了一个数组对象时,实际上就创建了一个引用数组,并且每个引用会自动初始化一个特定的值,该值拥有自己的关键字null,一旦Java看到了null,就知道这个引用还有指向对象,也就是没有被初始化。这个过程称为下标检查。

三、永远不需要销毁对象

    Java语言与其它语言一样都有作用域的概念,作用域决定了定义在其内部的变量的可见性和生命周期。作用域使用一对花括号表示。这里需要注意的是,Java对象与其它基本类型不同,它可以存活于作用域外部,如:

1
2
3
{
String s = new String("x");
}

    引用s在作用域结束时就已经失去了意义,但是String对象仍然存活,只是我们暂时无法使用它。因为它的唯一引用已经超出了范围。后面会讲程序的执行过程,如何传递和复制引用。Java中只要你需要,对象会一直存在,Java与其它语言不同的是自带了垃圾回收机制,会自动识别不需要的对象进行回收以达到释放内存的目的。因此你无需担心忘了释放对象的内存。

四、创建新的数据类型:类

    前文说到,同一种对象的集合我们称作是类,类表示所有由它实例化的对象都有相同的基本属性。Java中使用class关键字创建类,class后跟类的名字。这里不存在类似先有鸡还是先有蛋的问题,对象是一系列集合中的一个具体实例,也就是对象是一个类的具体实例化,因此在创建对象之前,一定先存在了这个对象所属的类:

1
class Students{}

如上我们定义了一个新的类,它是一个空类,没有属性也没有方法,所以还不能传递消息。但是我们可以用它实例化一个对象了:

1
Students student = new Student();

    类中有两种数据类型,一种是字段属性,另一种是方法。字段用来表示这个类的一些属性,方法用来表示这个类的一些行为,可以做哪些事情。每个对象都有存储字段的空间,并且类内部的普通属性的字段不可以共享。我们定义如下字段属性:

1
2
3
4
5
class Students{
int i;
double j;
boolean k;
}

    定义好了字段属性之后就可以创建一个对象,并通过对象引用操作对象内的字段属性,使用对象引用“.”方法的形式为对象内的字段属性赋值:

1
2
3
4
Students student = new Students();
student.i=1;
student.j=1.1;
student.k=true;

    Java会为类中的基本类型成员提供一个默认的初始值,以确保可以有效的初始化,防止程序运行出错。如果这个初始值不符合业务要求,那么需要开发者自行初始化。如果不是类中的基本类型数据变量,那么它的初始值就是随机的。

png2

五、方法、参数和返回值

    Java类中的方法决定了能做哪些事情,传递哪些消息。方法的组成包括:名称、参数、返回值和方法体,其基本形式如下:

1
2
3
ReturnType methodName(Arg arg){
/*Method Body*/
}

其中ReturuType是方法返回给调用方的返回值类型,methodName是方法名,Arg是参数类型,arg是参数,注释中的是方法要做的事情。

    Java中对象的调用同样是通过对象引用“.”方法名来完成的。并且要保证这个对象可以执行这个方法,否则编译器会提示错误:

1
2
objectName.methodName(arg,arg1,arg2);
ReturnType x = obejctName.methodName(arg,arg1,arg2);

如上分别是有返回值和无返回值的调用方式。

六、构建一个Java程序

    Java中为了解决命名冲突的问题,采用了分包的形式。将相同类型,或者相同作用,相同业务场景的类放在一个包中,不同的包彼此隔离,并且不同包中可以出现同名的类。使用import关键字导入相关的包即可访问对应包中的类,若导入后出现同名的类,则需要指明对应类的包名。通常情况下,我们必须创建一个类的实例对象来访问类中的属性和方法,而有一种特殊的情况,就是当被static修饰的属性和方法时,不需要对象进行访问。直接使用类名.属性/方法的形式进行访问。也就是说,这个属性和方法属于这个类了:

1
2
3
class TestStatic(){
static int i = 4;
}

    现在即使我们创建两个对象,对于属性i仍然只有一个值,也就是说这个被static修饰了的域变成了一个公共的属性。不同对象之间共享同一个变量。

1
2
TestStatic ts1 = new TestStatic();
TestStatic ts2 = new TestStatic();

关于static关键字的作用,下一章详细学习。

七、你的第一个Java程序

1
2
3
4
5
6
import java.util.*
public class HelloWorld{
public static void main(String[] args) {
System.out.println("Hello World");
}
}

    import是导入java.util工具包下的所有类,HelloWorld是类名,类名需要与文件名相同,main方法是程序的入口,虽然这里没有用到参数列表,但是对于main函数来说这是必须的。使用开发工具如Eclipse运行程序即可看到控制台打印“HelloWorld”

【Java编程思想】对象导论

发表于 2018-11-08 | 分类于 学习笔记 | 阅读次数:

一、抽象过程

    1.万物皆为对象:理论上来说,我们可以抽取待解决问题中任何一个概念化的构件,将其描述成为程序中的一个对象。

    2.程序是对象的集合,它们通过发送消息来告知彼此需要做的事:通俗来讲,程序是一系列对象的集合体,程序之间的通信可以细分到对象与对象之间的通信,要想进行通信就需要发送消息。从程序的角度来说,消息传递这个过程可以理解为一个方法的调用与执行。

    3.每个对象都可以由其它对象组成:换句话说,对象之间可以进行组合,形成新的具有具体意义的对象。

    4.每个对象拥有其自己的类型:类型用来区分不同对象的特征,比如正方形有四条边,三角形有三条边,边的条数就可以用来区分二者。

    5.某一特定类型的所有对象,都可以接收同样的消息:也就是说,具有相同类型的一系列对象,它们具有的行为是相同的,因此它们可以接收/处理相同的消息。

    对象具有行为、状态和标识,每一个对象都可以拥有内部数据和方法,数据用来表示对象的状态,方法用来产生特定的行为,每个对象有唯一的标识与其它对象区分。拥有相同类型的对象的集合称作类,也就是说我们创建的每一个对象,都来自一个类。

二、每个对象都有一个接口

    对象之间进行消息传递时是通过接口来完成的,一个对象的接口暴露给另一个对象,接口对应内部的一个具体实现,具体实现表示特定的方法行为。

三、每个对象都提供服务

    我们拆分、抽取的每一个对象,都是具有实际业务意义的,它们都应该能够传递具体的消息,从而提供特定的服务。

四、被隐藏的具体实现

    很多时候,我们将开发者分为两种,类创建者和客户端开发人员,所谓类创建者,可以理解为对象集合的构建者,客户端开发人员负责使用他们所创建的对象。有些时候,为了避免客户端开发者恶意修改类的功能,或者为了类在不断升级的过程中,能够更好地实现向下兼容,我们需要对客户端开发人员屏蔽一些他们用不到的且很关键的代码。这个屏蔽的过程叫做访问控制。

    Java中提供了三个级别的访问控制,分别为public、protected、private。public顾名思义就是所有人可见。private就是只有类的创建者可见。protected介于二者之间,在由基类派生出的派生类中,可以访问派生类中被protected修饰的域,而不能访问private域,其它与private相同。具体的访问控制后文继续学习。

五、复用具体实现

    有些时候我们需要使用一个现有的类完成特定的功能,但是又不能完成全部功能,那么我们就可以创建一个新的类,在这个类中引入先前的类的对象,并将其声明为private域,这样的话我们就可以使用这个对象的方法来完成一部分功能,然后在新类中创建新的属性和方法,并且在外人看来它就是一个新的类。这种“has-a”的关系我们称为组合。

六、继承

    当我们创建一个类时,如果另一个新类与这个类功能相似,我们仍然需要创建这个新类。解决这个问题的办法就是继承。继承虽然也是一个新类,但是这个类是由基类(俗称父类)衍生的导出类(俗称子类)。子类拥有父类所有非private的对象、属性、接口,此外可以根据不同的需要增加不同的功能。父类可以有很多个子类,它包含了子类的所有公共部分。如“几何形”是父类,每一个几何形都具有尺寸、颜色、位置等,同时每一个几何形都可以被绘制、擦除和移动。在此基础上可以导出它的子类“三角形”、“平行四边形”等,他们拥有父类的属性、方法之外还有自己独特的属性,例如有的形状可以被翻转等。
​ 子类不光继承了父类的属性,同时也继承了父类的方法,也就是说所有发给父类的消息,都可以发给子类。子类对接口的实现方法可以不改变,即访问子类的接口实际是与访问父类相同,当然也可以自己“覆盖”父类接口的方法,也就是说我和父类使用相同的接口,但是我们做不同的事情。同时如前边所说,子类也可以自己新增方法来满足自己的需求。前者不改变或者覆盖接口的方法,这种我们称作是“is-a”的关系,因为子类与父类本质的类型没有发生变化。而后边这种我们称作是“is-like-a”,因为在子类中增加了新的方法,所以这种相同的关系并不完全。

七、伴随多态的可互换对象

    我们说子类继承父类,同时继承了父类的方法,也就是说发给父类的消息同时能够发给子类。那么当我们把子类对象看成泛化的基类对象时,如果有个方法是让泛化的父类操作自己,那么编译器在编译时不知道该执行哪段代码的。一个非面向对象程序的函数调用是前期绑定,函数要执行的代码在程序运行之前就已经确定了,然而在面向对象中,直到程序执行我们才知道哪段代码被执行了。所以为了解决消息执行哪段代码的问题,Java使用了后期绑定的概念,使用一小段特殊的代码来代替非面向对象中所说的绝对地址调用,这段代码使用在对象中存储的信息来计算方法的地址。比如有个父类Shape和子类Circle、子类Triangle,父类中有如下方法

1
2
3
4
5
void doSomething(Shape shape){
shape.erase();
//...
shape.draw();
}

这个方法可以与任意Shape类型的对象交互,如果程序中有它的子类调用了该方法

1
2
3
4
Circle circle = new Circle();
Triangle triangle = new Triangle();
cicle.doSomething();
triangle.doSomething();

八、单根继承结构

    在Java语言中,所有的类都是Object类的子类,拥有着Object类的基本方法。这种单根继承结构有很大的好处,比如在垃圾回收中可以避免由于不知道对象的类型而无处下手,因为他们都可以使用Object类的方法,并且所有对象都可以很容易的在堆上创建。

九、容器

    有时候我们并不知道处理一个问题需要多少个对象,或者他们需要存活多久,那么我们就不知道该怎么样存储这些对象。Java语言中创建了一种对象类型,叫做容器(也叫集合)。这种新的对象类型内部有对其他对象的引用,当你需要的时候你可以很容易的扩充容器的空间来容纳对象。Java中提供了多种容器类型,如List(用于存储序列)、Map(也被称为关联数组,用来建立对象的关联)、Set(每种对象类型只持有一个)。因为容器中存储了其他对象的引用,所以根据单根继承结构,容器中的对象类型都是Obejct类型,这样很方便其他对象类型进行转型。但是从Object类型转到其它特定类型(称作向下转型)很容易发生错误,所以Java SE5之后引入了泛型的概念,参数传递使用一对<>,<>内部可以传递任意类型的对象,这便解决了向下转型带来的安全隐患。

十、对象的创建和生命周期

    对象的创建主要有两种,一种是在编程时由程序员控制,将对象置于栈中或者是静态区域,这种的好处是知道对象的大小、生命周期。同时也限制了对象的灵活性。另一种是通过new在内存堆中动态的创建对象,这种方式的好处是灵活,需要的时候直接在内存中创建即可,实现了存储空间的动态管理。Java中完全采用了动态内存的分配方式。

    对于生命周期,栈上的生命周期,编译器可以知道它什么时候销毁,并自动回收。而在堆上创建的对象,必须由程序员自己指定回收时间,如果没有指定则会造成内存泄漏。Java中提供了垃圾回收机制,可以自动的回收在堆上创建的对象。

十一、异常处理:处理错误

    Java内置了异常处理机制,相当于一条与正确执行并行的路线,当程序发生异常时会执行异常的代码。同时,允许程序在异常中进行处理并返回到正确的结果中去。

十二、并发编程

    Java与其他语言一样,提供多线程的并发编程方式,提高程序运行效率。

十三、Java与Internet

    Java不仅可以编写客户端程序,还可以编写网络Web应用程序。

1…34

Crayon Cxy

Go over the mountain, and they will hear your story.

38 日志
3 分类
14 标签
友情链接
  • 六脉神间
  • My CSDN
© 2019 Crayon Cxy
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4