本文用于对java基础知识的整理,帮助读者查缺补漏。
Java : 一门面向对象的语言
面向过程让计算机有步骤地顺序做一件事,是过程化思维,使用面向过程语言开发大型项目,软件复用和维护存在很大问题,模块之间 耦合严重 。面向对象相对面向过程更适合解决规模较大的问题,可以拆解问题复杂度,对现实事物进行抽象并映射为开发对象,更接近人的思维。
例如开门这个动作,面向过程是 open(Door door),动宾结构,door 作为操作对象的参数传入方法,方法内定义开门的具体步骤。面向对象的方式首先会定义一个类 Door,抽象出门的属性(如尺寸、颜色)和行为(如 open 和 close),主谓结构。 即,面向过程以open为核心,面向对象以Door类为核心。
封装
把客观事物封装成类,且 类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏 。封装是面向对象的特征之一,是对象和类概念的主要特性。简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
封装的核心就是访问权限控制
我知道你听不懂
听起来,似乎封装和”保护“”访问权限“这个词有关系,让你想到了黑客什么的,进而让你不求甚解,实则当我们看到访问权限的时候,应该想到”角色“这个词,就像我们使用linux的root角色一样。
就像root和非root一样,我们将代码分成两类, private
和 public
,当我们开发一些为他人提供帮助的类库时,其他用户可以随意使用public的代码,而我们通过修改 private
的代码,就可以实现再完全不影响用户的情况下更新。(当然客户端程序员也可以将精力聚焦于public的代码)
而在此基础上,除了其他用户和我们自己,代码与代码之间也需要共享功能(或者说模块耦合,我尽可能避免这种表达),因此我们引出了 protected
的概念,Java通过 package
个内聚的类库单元中。Java 中通过 package 关键字加以控制。
包管理
包里面包含几个类,他们被组织在一个单独的命名空间(namespace)下。 然而,如果每次使用类都要写具体包名,无疑是繁琐的,因此我们使用 import
import
- import 包名.*; 导入整个包
- import 包名.类名 导入指定类
为了防止包名冲突,我们可以建立一个包将所有代码都放入这个包种,使用类似操作系统多级文件的方式解决冲突,同时在命名上,按照惯例,通过反顺序的Internet域名来保证其独一无二。
访问权限
- 默认,空白
- 包访问权限
- 有时候表示为friendly
- 当前包中所有其他类对这个成员都有访问权限
- public
- 接口访问权限
- 构造器一般为public,这样包外类才能使用该类(除了一些设计模式会不使用public)
- private
- 私有成员:你无法访问
- 除了包含该成员的类之外,其他任何类都无法访问这个成员通常用于隐藏措施
- protected
接口与实现
访问控制通常被称为隐藏实现(implementation hiding)。将数据和方法包装进类中并把具体实现隐藏被称作是封装(encapsulation)。 访问控制在数据类型内部划定了边界。 第一个原因 是确立客户端程序员可以使用和不能使用的边界。可以在结构中建立自己的内部机制而不必担心客户端程序员偶尔将内部实现作为他们可以使用的接口的一部分。 第二个原因 :将接口与实现分离。如果在一组程序中使用接口,而客户端程序员只能向 public 接口发送消息的话,那么就可以自由地修改任何不是 public的事物(例如包访问权限,protected,或 private 修饰的事物),却不会破坏客户端代码。
为了清晰起见,你可以采用一种创建类的风格:public 成员放在类的开头,接着是 protected 成员,包访问权限成员,最后是 private 成员。这么做的好处是类的使用者可以从头读起,首先会看到对他们而言最重要的部分(public 成员,因为可以从文件外访问它们),直到遇到非 public 成员时停止阅读。
类访问权限
访问权限修饰符也可以用于确定类库中的哪些类对于类库的使用者是可用的。如果希望某个类可以被客户端程序员使用,就把关键字 public 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。
这里有一些额外的限制:
- 每个编译单元(即每个文件)中只能有一个 public 类。这表示,每个编译单元有一个公共的接口用 public 类表示。该接口可以包含许多支持包访问权限的类。一旦一个编译单元中出现一个以上的 public 类,编译就会报错。
- public 类的名称必须与含有该编译单元的文件名相同,包括大小写。所以对于 Widget 来说,文件名必须是 Widget.java,不能是 widget.java 或WIDGET.java。再次强调,如果名字不匹配,编译器会报错。
- 虽然不是很常见,但是编译单元内没有 public 类也是可能的。这时可以随意命名文件(尽管随意命名会让代码的阅读者和维护者感到困惑)。
注意,类既不能是 private 的(这样除了该类自身,任何类都不能访问它),也不能是 protected 的。所以对于类的访问权限只有两种选择:包访问权限或者 public。为了防止类被外界访问,可以将所有的构造器声明为 private,这样只有你自己能创建对象(在类的 static 成员中):
继承
是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。继承概念的实现方式有二类:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力。
如前文所说,面向对象相对于面向过程,是希望可以实现高内聚,低耦合,换句话说,面向对象中的三个特征:继承、封装、多态,都是为了高内聚低耦合实现的,这里讲的继承则是为了高内聚,一个模块做到了高内聚,其代码可读性、复用性都得到了保证,维护起来也更加的方便。
而代码想要复用,有两种方式:,
-
组合:在新类中创建现有类的对象。这种方式叫做 “组合”(Composition),通过这种方式复用代码的功能,而非其形式。
-
继承:创建现有类类型的新类。照字面理解:采用现有类形式,又无需在编码时改动其代码,这种方式就叫做 “继承”(Inheritance),编译器会做大部分的工作。继承是面向对象编程(OOP)的重要基础之一。
组合
其实组合对于我们来说很常见,因为就算是一个普通的类,也是由一些基本属性(int,double,char)组和非基本属性(类,常见的String) 组合 而成 需要注意的是,编译器不会为每个引用创建一个默认对象,这是有意义的,因为在许多情况下,这会导致不必要的开销。初始化引用有四种方法:
- 当对象被定义时。这意味着它们总是在 调用构造函数之前初始化 。
- 在该类的构造函数中。
- 在实际使用对象之前 。这通常称为延迟初始化。在对象创建开销大且不需要每次都创建对象的情况下,它可以减少开销。
- 使用 实例初始化 。
上面这些繁杂的顺序让人困惑,在这里讲解下
一、类初始化过程
1.一个类要创建实例需先加载并初始化该类
(1)、main方法下的类需要先加载和初始化
(2)、一个子类初始化需要先初始化他的父类
(3)、一个类初始化就是执行clinit ()方法
说明: clinit ()方法由静态变量显示赋值代码和静态代码块组成,静态变量显示赋值代码和静态代码块从上到下顺序执行,clinit方法只执行一次
二、实例初始化过程
1.实例初始化就是执行init()方法
(1)init()方法可能重载有多个,有几个构造器就有几个init()方法
(2)init()方法由非静态变量显示赋值代码和非静态代码块、对应构造器代码组成
(3)非静态变量显示赋值代码和非静态代码块从上到下顺序执行,最后执行构造器代码
(4)每次创建实例对象,调用对应构造器,执行的都是init()方法
(5)init()方法的首行是super()或super(实参列表),即对应父类的init()方法
方法的重写Override
1.哪些方法不可以被重写
(1)、静态方法
(2)、private方法(子类方法不可见)
(3)final方法
2.对象多态性
(1)、子类如果重写了父类的方法,通过子类调用的一定是子类重写过的方法
(2)、非静态方法默认调用的是this
(3)、this对象在构造器或者说()方法中就是正在被创建的对象
类的初始化和加载
Java 中万物皆对象,所以加载活动就容易得多。记住每个类的编译代码都存在于它自己独立的文件中。该文件只有在使用程序代码时才会被加载。一般可以说 “类的代码在首次使用时加载 “。
这通常是指创建类的第一个对象,或者是访问了类的 static 属性或方法。构造器也是一个 static 方法尽管它的 static 关键字是隐式的。(所以clint()方法要初始化静态代码和构造方法)
继承
继承是所有面向对象语言的一个组成部分。事实证明,在创建类时总是要继承,因 为除非显式地继承其他类,否则就隐式地继承 Java 的标准根类对象(Object)。组合的语法很明显,但是继承使用了一种特殊的语法。当你继承时,你说,“这个新类与那个旧类类似。你可以在类主体的左大括号前的代码中声明这一点,使用关键字extends 后跟基类的名称。
一个子类方法的内部,不能直接调用父类的同名方法(谜底在谜面上,因为同名,不知道我说清楚了没有),因此需要 super
关键字, super.method()
。子类在继承和重写父类的旧方法的同时,也可以添加自己的新方法
如上文所言,子类初始化之前,父类会先初始化,这里不赘述了。
子类可以 重载
父类的方法
注意!我说的不是重写,为了防止出现重写因为拼写错误或者粗心不小心重载的问题,可以使用注释@Override.
关于重写重载区别,稍后介绍
向上转型
子类可以当作父类使用
class Son extend Father{
@Override
public void play();
}
class Father{
public void play();
public static void tune(Father f){
f.play();
}
}
Son son = new Son();
Father.tune(son);
选择组合还是继承?
从复用的角度讲,组合还是继承都又可以实现代码的复用,很多人通过语义来理解,比如说机器和汽车、汽车和轮子,看起来是一道阅读理解题,然而这样并不牢靠。
一种判断使用组合还是继承的最清晰的方法是问一问自己是否需要把新类向上转型为基类。如果必须向上转型,那么继承就是必要的,但如果不需要,则要进一步考虑是否该采用继承。只要记住问一问 “我需要向上转型吗?”,就能在这两者中作出较好的选择。
Final关键字
- 用于数据
- 一个用不改变的的编译时常量(编译时计算),一个被 static 和 final 同时修饰的属性只会占用一段不能改变的存储空间。(static代表只有一个)
- 一个在运行时初始化就不会改变的值(对象引用)这类
final
其实还是可以改变对象内部的值的,尤其是数组, final
显得没什么用
- 空白final(没有直接初始化的final)
- 用在参数上(详情见后文)
- 用于方法
- 给方法上锁,防止子类通过覆写改变方法的行为。这是出于继承的考虑,确保方法的行为不会因继承而改变。
- 在早期的 Java 实现中,如果将一个方法指明为 final,就是同意编译器把对该方法的调用转化为内嵌调用。当编译器遇到 final 方法的调用时,就会很小心地跳过普通的插入代码以执行方法的调用机制(将参数压栈,跳至方法代码处执行,然后跳回并清理栈中的参数,最终处理返回值),而用方法体内实际代码的副本替代方法调用。这消除了方法调用的开销。但是如果一个方法很大,代码膨胀,你也许就看不到内嵌带来的性能提升,因为内嵌调用带来的性能提高被花费在方法里的时间抵消了。(现在JVM可以对代码膨胀进行优化)
- 用于类
- 当说一个类是 final (final 关键字在类定义之前),就意味着它不能被继承。之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类。
多态
多态提供了另一个维度的接口与实现分离
是指[一个类实例]的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
代码中的修改不会破坏程序中其他不应受到影响的部分。 多态是一项 “将改变的事物与不变的事物分离” 的重要技术。
这可能就是你对多态的理解,但本文并不想纠结于动态绑定如何优于前期绑定
再谈构造器调用顺序
- 分配给对象的存储空间会被初始化为二进制 0。
- 基类构造器被调用。这个步骤被递归地重复,这样一来类层次的顶级父类会被最先构造,然后是它的派生类,以此类推,直到最底层的派生类。
- 按声明顺序初始化成员。
- 调用派生类构造器的方法体。
销毁的顺序正好相反
在我看来,多态更像是封装和继承所产生的结果,三者共同被描述为面向对象。他们带来更快的程序开发、更好的代码组织、扩展性更好的程序和更易维护的代码