最近因公司需要远程访问客户的一种设备,但他们不想公开开放,但他们设备有一个openvpn配置项,所以就研究了下,搭建完记录下
环境:
openvpn服务器系统版本 CentOS Linux release 7.6.1810 (Core)
客户端连接软件安装在windows10
搭建及要实现的内容如下:
1、server端和client端的安装和配置
2、配置防火墙及路由转发
3、配置账号密码验证,给账号分配固定IP,且用户连接与断开实时调用后续处理脚本
安装easy-rsa
和openvpn
软件包
1 | yum install openvpn easy-rsa |
复制相关文件
1 | //注意easy-rsa的版本号,你的有可能不一样 |
/share/easy-rsa/3.0.7/* /etc/openvpn/easy-rsa/
编辑vars文件,修改如下选项(此步骤为可选操作,使用默认的也是可以的)
1 | vi /etc/openvpn/easy-rsa/vars |
初始化证书目录
1 | cd /etc/openvpn/easy-rsa/ |
创建CA根证书
1 | ./easyrsa build-ca |
创建服务器端证书,执行命令一路回车
1 | ./easyrsa gen-req server nopass |
签署服务器端证书
1 | ./easyrsa sign server server |
生成加密交换时的Diffie-Hellman文件,会生成一个pem后缀文件,生成过程比较慢
1 | ./easyrsa gen-dh |
1 | vi openvpn/server.conf |
创建用户账号和密码的配置
1 | touch /etc/openvpn/pwd-file |
给用户client1分配固定IP为10.8.0.57
1 | mkdir openvpn/ccd |
创建密码验证脚本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
34
35touch /etc/openvpn/checkpwd.sh
chmod +x checkpwd.sh
vi /etc/openvpn/checkpwd.sh
# 脚本内容
PASSFILE="/etc/openvpn/pwd-file"
LOG_FILE="/etc/openvpn/logs/openvpn-password.log"
TIME_STAMP=`date "+%Y-%m-%d %T"`
readarray -t lines < $1
username=${lines[0]}
password=${lines[1]}
if [ ! -r "${PASSFILE}" ]; then
echo "${TIME_STAMP}: Could not open password file \"${PASSFILE}\" for reading." >> ${LOG_FILE}
exit 1
fi
CORRECT_PASSWORD=`awk '!/^;/&&!/^#/&&$1=="'${username}'"{print $2;exit}' ${PASSFILE}`
if [ "${CORRECT_PASSWORD}" = "" ]; then
echo "${TIME_STAMP}: User does not exist: username=\"${username}\", password=\"${password}\"." >> ${LOG_FILE}
exit 1
fi
if [ "${password}" = "${CORRECT_PASSWORD}" ]; then
echo "${TIME_STAMP}: Successful authentication: username=\"${username}\"." >> ${LOG_FILE}
exit 0
fi
echo "${TIME_STAMP}: Incorrect password: username=\"${username}\", password=\"${password}\"." >> ${LOG_FILE}
exit 1
(可选配置)争对client-connect和client-disconnect这两个配置这里就不多说了,可能对大多数人用处不大,我的业务是有用户连接上了,就调用脚本向远程http接口通知我们系统有客户连接上了然后去做业务处理的,我把大致脚本发出来
1 | connect.sh |
开启内核路由转发功能
1 | echo "net.ipv4.ip_forward = 1" /etc/sysctl.conf |
配置防火墙
1 | # 开放tcp 1194端口 |
启动openvpn服务器
1 | openvpn /etc/openvpn/server.conf & |
执行start启动服务命令后,使用status命令查询状态,如果Active: failed
,请到/etc/openvpn/logs/openvpn.log
目录下查看日志,基本打印的错误消息挺全的,可借此排查错误,另外记得如果开启了防火墙要把openvpn配置的端口开放,并且有的服务商还在他们自己控制台有安全策略,如果服务启动了连接很久都连接不上可以往这些方面思考一下
创建配置文件
1 | cd |
然后导出client.ovpn
文件,导入到客户端里进行连接
openvpn客户端官方下载连接
https://openvpn.net/community-downloads/
下载安装时注意如果弹出要安装TAP-Windows Provider V9
的东西一定要通过,不然安装成功了,连接时还是无法连接
客户端
启动客户端软件,然后在任务栏有一个像电脑显示器的小图标右键选择导入配置文件
连接
在弹出的密码框里输入我们在服务端pwd-file
文件里配置的用户名和密码
连接成功,且IP也是成功分配到我们配置的IP
一些有帮助的文章:
https://i4t.com/4481.html
https://my.oschina.net/stache/blog/1512610
http://blog.leanote.com/post/mrnice/openvpn%E6%90%AD%E5%BB%BA
做一行,爱一行,即使是不爱,也要做好
]]>之前一直用的是php博客系统,但奈何我发现近些时间不是很喜欢折腾博客了,只想有事写两篇,没事放着生蛋,就想着换到github上来在保留绝对控制权的情况下让github托管着,在jekyll和hexo比较中看到了hexo主题中被人移植到hexo的Maupassant模板,感觉很适合我,于是借鉴cho大神最新版的Maupassant主题,结合其他博主(例:屠城博客)的一些扩展上进行了移植与修改
由于自己特爱简洁,所以移植了最基本的功能,加入了少量的博客可能用得着的其他小扩展,有用得着的朋友可以进入github下载
https://github.com/7ye/maupassant-hexo
1 | fancybox: true ## 是否启用Fancybox图片灯箱效果 true/false. |
true
source
目录下建立相应名称的文件夹首页默认显示文章摘要而非全文,可以在文章的front-matter
中填写一项description:
来设置你想显示的摘要,或者直接在文章内容中插入<!--more-->
以隐藏后面的内容。
若两者都未设置,则自动截取文章第一段作为摘要。
对于首页的description
,可在Hexo下的配置文件_config.yml
下配置description
参数,对于文章,可在front-matter
中填写一项description:
来手动写描述,如果文章没有任何配置,则自动截取文章前150个字符为当前文章的description
(不包括html标签)
在source
目录下建立相应名称的文件夹,然后在文件夹中建立index.md
文件,并在index.md
的front-matter
中设置layout为layout: page
。若需要单栏页面,就将layout设置为 layout: single-column
。
文章和页面的评论功能可以通过在front-matter
中设置comments: true
或comments: false
来进行开启或关闭(默认开启)。
要启用代码高亮,请在Hexo目录的_config.yml
中将highlight
选项按照如下设置:
1 | highlight: |
要启用数学公式支持,请在Hexo目录的_config.yml
中添加:
1 | mathjax: true |
并在相应文章的front-matter
中添加mathjax: true
,例如:
1 |
|
数学公式的默认定界符是$$...$$
和\\[...\\]
(对于块级公式),以及$...$
和\\(...\\)
(对于行内公式)。
但是,如果你的文章内容中经常出现美元符号“$
”, 或者说你想将“$
”用作美元符号而非行内公式的定界符,请在Hexo目录的_config.yml
中添加:
1 | mathjax2: true |
而不是mathjax: true
。 相应地,在需要使用数学公式的文章的front-matter
中也添加mathjax2: true
目前支持简体中文(zh-CN),需其他语言支持在主题目录下languages
,按照已有配置文件对照着规则添加yml
配置文件即可
但是,如果别人在问一下
Integer i1 = 1;
int i2 = 1;
i1==i2 为true还是false?
Integer i3 = 1;
Integer i4 = 1;
i3==i4 为true还是false?
要求解释一下,为什么会是这个结果,可能会有一些人脑袋就有点懵,虽说这很基础,但很多人没有这么深入研究过,这里对他进行一下总结;
首先我们先来看一段代码
1 | public class TestInteger { |
第一处,i1和i2比较,i1和i3比较,结果都为true,在JDK1.5以上,Integer和int比较都会进行自动拆箱,所以这里为true
第二处,i4和i5比较的时候为true,但i6和i7进行比较的时候为false,在这里就会有非常多的人不知道是为什么,这里其实跟编译有关
1 | Integer i4 = 127; //在Java编译的时候,被翻译成Integer i4 = Integer.valueOf(127); |
所以,这里我们在JDK源码里面查看一下valueOf(int i);函数源码的实现
1 | /** |
在JDK的源码中,找到一段valueOf(int i);的实现代码,发现JDK在这个实现里对-128到127之间的数进行了缓存(这样对效率和空间上都比较好),如果数值大于了127,返回的就是一个对象,而如果两边对比的都是大于127的,那么就相当于对比的两个对象,所以会为false
i8和i9不一样,为因为对象不一样,所有为false
总结:
1、Integer和int进行比较,不管有没有进行new,都会为true,因为会把Integer自动拆箱成int再去比
2、两个都是非new出来的Integer,如果数值在-128到127之前,则为true,否则为false
3、两个new出来的Integer进行对比,因为对象不一样会为false
Clone有缺省行为,super.clone();他负责产生正确大小的空间,并逐位复制,使用clone()来复制一个对象,clone()从Object类继承。所有具有clone功能的类都有一个特性,那就是它直接或间接地实现了Cloneable接口。
我们可以来看一看JDK的Object源码里clone接口
1 | protected native Object clone() throws CloneNotSupportedException; |
可以看出它是一个protected方法,所以我们不能简单地调用它;关键字native,表明这个方法使用java以外的语言实现
Clone的使用方式:
1 | public class User implements Cloneable{ |
示例:
1 | User u = new User(); |
说明:
1、拷贝对象返回的是一个新对象,而不是一个引用
2、拷贝对象与用new操作符返回的对象区别在于这个拷贝包含了一些原来对象的信息,而不是对象的初始化信息
浅拷贝(浅克隆)和深拷贝(深克隆)的概念:
浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。
深拷贝:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
]]>本文要说的是html5的localStorage本地储存方式,localStorag可以说是cookie的一个加强版,相比localStorag来说,cookie的限制太多,限制大小4K,并在某些浏览器下还有域名限制;好的是现在html5提供的localStorag比较牛*,有5M的大小,因此爱折腾的小伙伴可以用它来做一些有意思东西
使用localStorage(本地缓存)的优缺点:
优点:
缺点:
对于本文要做的事情就是把js和css静态文件保存到本地储存,所以在我们眼里,缺点基本上可以忽略不计
实现代码: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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108var whir = window.whir || {};
whir.res = {
//页面版本,也由页面输出,用于刷新localStorage缓存
pageVersion: "0.0.1",
loadJs: function (name, url, callback) {//动态加载js文件并缓存
//判断浏览器是否支持HTML5本地储存
if (window.localStorage) {
var xhr;
var js = localStorage.getItem(name);
if (js == null || js.length == 0 || this.pageVersion != localStorage.getItem("version")) {
if (window.ActiveXObject) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
} else if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
}
if (xhr != null) {
xhr.open("GET", url);
xhr.send(null);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
js = xhr.responseText;
localStorage.setItem(name, js);
localStorage.setItem("version", whir.res.pageVersion);
js = js == null ? "" : js;
whir.res.writeJs(js);
if (callback != null) {
callback(); //回调,执行下一个引用
}
}
};
}
} else {
whir.res.writeJs(js);
if (callback != null) {
callback(); //回调,执行下一个引用
}
}
} else {
whir.res.linkJs(url);
}
},
loadCss: function (name, url) {//动态加载css文件并缓存
if (window.localStorage) {
var xhr;
var css = localStorage.getItem(name);
if (css == null || css.length == 0 || this.pageVersion != localStorage.getItem("version")) {
if (window.ActiveXObject) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
} else if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
}
if (xhr != null) {
xhr.open("GET", url);
xhr.send(null);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
css = xhr.responseText;
localStorage.setItem(name, css);
localStorage.setItem("version", whir.res.pageVersion);
css = css == null ? "" : css;
css = css.replace(/\..\/img\//g,+"http://www.test.com/img/"); //css里的img路径需单独处理
whir.res.writeCss(css);
}
};
}
} else {
whir.res.writeCss(css);
}
} else {
whir.res.linkCss(url);
}
},
//往页面写入js脚本
writeJs: function (text) {
var head = document.getElementsByTagName('HEAD').item(0);
var link = document.createElement("script");
link.type = "text/javascript";
link.innerHTML = text;
head.appendChild(link);
},
//往页面写入css样式
writeCss: function (text) {
var head = document.getElementsByTagName('HEAD').item(0);
var link = document.createElement("style");
link.type = "text/css";
link.innerHTML = text;
head.appendChild(link);
},
//往页面引入js脚本
linkJs: function (url) {
var head = document.getElementsByTagName('HEAD').item(0);
var link = document.createElement("script");
link.type = "text/javascript";
link.src = url;
head.appendChild(link);
},
//往页面引入css样式
linkCss: function (url) {
var head = document.getElementsByTagName('HEAD').item(0);
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.rev = "stylesheet";
link.media = "screen";
link.href = url;
head.appendChild(link);
}
};
因为每当静态文件储存到了本地缓存之后,之后除非是手动删除不然就会一直存在,上面的代码在读取了一次之后,以后再次加载的时候就不会再去加载,而是直接去本地储存里读取,所以如果当css或js静态文件有改动的时候需要更改版本号,就需要手动更改pageVersion
版本号参数
页面加载及添加js缓存调用方法
参数:[缓存名称,文件加载路径,回调方法(可选)]
1 | whir.res.loadJs("jquery.js", "//cdn.bootcss.com/jquery/1.12.4/jquery.min.js",null); |
还有另外一种加载情景,由于js加载有顺序要求,所以需要将后加载的脚本作为前一个脚本的回调参数传入,不然可能会出现有的js脚本需要jquery的支持,但在jquery加载之前执行了,会因为jquery未加载完而报错
1 | whir.res.loadJs("jquery", "//cdn.bootcss.com/jquery/1.12.4/jquery.min.js", |
页面加载及添加css缓存调用方法
参数:[缓存名称,文件加载路径]
1 | whir.res.loadCss("css", "/style/style.css", null); |
注:在加载网络静态文件的时候,会容易出现相对路径加载不正确的情况,例如图片、字体文件,因为加载到本地储存就相当于这个文件变成了当前网站本地的css文件,与网络文件已经无关了,但里面的相对路径也是基于当前网站进行读取文件,而网络文件是基于他自己域名进行获取的路径,所以会加载失败,因此在css文件加载完成之后需要对相对路径做特殊处理
1 | //css里的img路径需单独处理 |
用加入本地缓存的方法把静态文件缓存到本地,可以大大的提高网站的响应速度,在网站css和js要加载很多的情况下,加入到本地缓存也是一个不错的方法,从几十个请求,可以立马变成几个请求,唯一需要注意的就是对于网络路径和第一次脚本的加载顺序、效果方面需要费心做一下处理,对于小站点和个人博客来说用这种方法挺好的
]]>之前在APP里面接入达达配送的时候,看到他们提供的API接口里面大致有了这个一个认证的过程,自己琢磨了一下,依稀的弄出了一个类似这样的自动登录的流程;
这一步是需要用户使用账号和密码进行登录去获取,用户如果登录成功,那么后台返回一个token及token的失效时间,及未来如果token失效之后刷新token的令牌!
返回参数示例及说明:
1 | { |
说明:
此接口是对第一个接口的一些弥补,token的过期时间原理上可以说是越短越安全,那么存在的问题就是token有一个过期时间,那么过期之后是不可能让用户重新输入用户名和密码来重新获取token的,那样完全违背了提升体验的初衷,相当于此功能是一个累赘了;
在token过期的情况,我们可以使用保存在手机本地的refresh_token去后台刷新一下token,重新获取一组令牌信息,覆盖掉以前保存在手机上的令牌信息,这也算是提升了一点安全性,为避免以前的令牌信息如果真落入别人之手;
此接口的请求参数可以参考以下参数:
1 | { |
返回参数示例及说明:
1 | { |
说明:
看着是不是和获取token的接口返回值一样,对,你没有看错,这确实就是和获取token的接口是一样的,把返回的这些令牌信息覆盖掉之前保存在手机上的老的令牌信息就可以了;特别是这里为了节省请求接口的次数,刷新token成功之后就可以让用户跳转到首页进行使用app了,不用再次请求其他认证token的接口了
从请求到响应之后的一系列处理流程为:
此接口相当而言算是请求频率比较多的一个接口,他对token进行认证成功和失败有不同的处理方式;
此接口的请求参数可以参考以下参数:
1 | { |
返回参数示例及说明:
1 | { |
说明:
概述:单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类
作用:Singleton模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在
注意点:构造函数是私有的,避免外界利用构造方法直接创建出实例
使用场景:
优点:
缺点:
Singleton的实现有多种方式,一般单例都是五种写法。饿汉,懒汉,双重校验锁,枚举和静态内部类。
1、饿汉
1 | public class EagerSingleton { |
2、懒汉
1 | public class LazySingleton { |
3、双重检查锁定(算是懒汉式的一种改进)
1 | public class Singleton { |
4、枚举
1 | public enum EnumSingleton { |
5、静态内部类
1 | public class StaticInsideClassSingleton { |
整理途中参考的一些文章
http://www.cnblogs.com/ykt/archive/2011/11/24/2261251.html
http://www.oschina.net/code/snippet_107039_6062
http://www.tuicool.com/articles/NVza2am
http://www.blogjava.net/kenzhh/archive/2016/03/28/357824.html
钻了一个牛角尖,构造器为什么不能被继承呢?
在解释构造器Constructor为什么不能被继承和重写之前,我们先来了解构造器的概念。构造器在程序语言中是为了创建一个类的实例,如:Student stu1= new Student(),这就是创建了一个类的实例,同时也生成了一个构造器。构造器也分为有参数的和无参数的,上面的例子是无参的,Student stu2 = new Student(“李四”,28),这是有参的构造器。
那为什么说构造器Constructor为什么不能被继承和重写呢?
因为“重写”只能发生在“继承”或“接口”这个两个概念上。我们可以以动物来举例,如果我们声明的这个动物用了new语句,这时我们就创建了一个实实在在的动物出来了,那么这个动物是独立存在的,是一个体,一个独立存在的的动物当然不能够被继承。但是对于类在没有实体化之前就是一个抽象的概念,那么就可以被继承。
如果还不能被理解,对于有一些钻牛角尖的人还需要问为什么的话,可以有一个很牛B的回答:你和你爸爸这是实体,独立存在的,那么就是一个构造器,如果构造器可以继承的话,那么,你爸爸、你爷爷、你、甚至往上,这些所有的人都完全一样的了。
]]>封装隐藏了类的内部实现机制,可以在不影响使用情况下改变类的内部结构,同时也保护了数据,对于外界而言他的内部细节是隐藏的,暴露给外界的只是他的访问方法
继承所描述的是“is-a”
(是一个,说明该类是某类的一个特殊例子)的关系,如果有两个对象A和B,若可以描述为“A是B”,则可以表示 A 继承 B,其中 B 是被继承者称之为父类或者超类,A 是继承者称之为子类或者派生类。
多态成立的三个条件
1.继承
2.子类重写父类方法
3.父类引用指向子类对象
用简述的话说就是:继承、重写、向上转型
从某一个角度来讲,封装和继承都相当于是为多态而做准备
多态应用的一大前提是继承,以及继承里面的重写方法;继承的一大前提是封装,及封装里面涉及到重要知识点方法重载
多态用一句话概括可以描述为:事物在运行过程中存在不同的状态;他的重要特性:一个接口,多种实现;指在编译时引用变量调用的方法无法确定是执行A类里面的方法还是B类里面的方法,只有在运行期间才能确定要执行的方法,这样不用修改源代码,就可以改变程序运行时所绑定的具体代码,让程序有多个状态可以选择,这就是多态性
举个可爱的、形象的例子,比如有一个函数方法是对描述动物的特征,方法要求传递的参数要求是动物,但我们并不知道到底是何种动物,只有听着声音才能判断这是什么动物,听到“瞄”叫声,发现这是猫、听到“旺”叫声,发现这是狗、听到“咩”叫声,发现这是羊,于是可以描述成这样:
动物a = 猫
动物b = 狗
动物c = 羊
这里猫、狗、羊都是动物的子类,我们可以通过动物这个父类就能够引用到不同的子类,这就是多态;只有在运行的时候才会知道应用变量所指向的具体对象
由上面的例子可以看出,猫(Cat)、狗(Dog)、羊(Sheep)是动物(Animal)的子类,把Animal指向Cat对象。因为Cat是继承的Animal对象,所以Cat可以转为Animal;这样做的好处就是因为子类继承自父类,所以他可以提供比父类更强大的功能,我们定义了一个指向子类的父类引用类型,他除了能引用父类的方法外,还可以使用子类的强大功能,我们把这样的描述称为向上转型
向上转型需要注意的是父类类型的引用只能调用父类中定义的属性和方法,对于只存在子类的方法和属性他就不能调用了,即调用的方法和属性需是在超类中存在的方法
1 | public class Animal { |
上面的结果由于子类重载了父类的fun1()方法,重写了fun2()方法,重载后的fun1(String s)和fun1()不是同一个方法,父类中由于没有过该方法,向上转型后,丢失了fun1(String s)方法,就执行的父类的fun1(),调用fun2()方法的时候,由于子类重写的fun2(),是能调用到子类的fun2()的
多态定义上就是要通过统一、抽象的形式去完成复杂多变的需求,比如
1 | animal.features(); |
animal是不同的,但是属于一个派生系,features形式上是相同的,但实现不同,具体定位到哪个实现由animal的本质决定,这对于jvm来说已经非常清楚了,动态加载,动态绑定,简单粗暴的一句话就是调用了相同的方法,出现了不同的结果,这就是多态的表现
总结:因为有向上转型,所以指向子类的父类引用,只能调用父类里面存在的方法和属性;子类重写父类的方法,必定能被父类引用调用
多态有两种实现形式:继承和接口
上面的例子描述的就是继承的实现,那么接口的实现是怎样的呢?
1 | //定义接口InterA |
上例中,B类和C类是InterA接口的实现类,分别实现了接口fun(),通过将B类和C类的实例赋给接口引用a实现了方法在运行时动态绑定,也满足了“一个接口,多种实现”的特性,展出出了java的多态性
总结:同继承实现相似,java在利用接口调用实现类的方法的时候,该方法也必须是在接口中存在,并在实现类中被重写;
对比继承和接口两种实现形式,继承由于是单继承,但接口可以多实现,所以在进行扩展的时候,接口相比继承的方式有更好的灵活性
通过一个实例巩固下,这个实例包含了太多的知识,是关于多态的一个非常经典的例子:
1 | class A { |
上例实例输出的最终结果为:
1 | 1--A and A |
先看比较好理解的1、2、3,①会到A类去找,但由于没发现B类型,B类型向上转型为A,最终执行的效果就是a1.show(A obj);②同理也是到A类去找,由于没有发现C类型,也是由C向上转型为A,最终执行效果同①类似,打印都是“A and A”,③这里直接A类能找到直接打印
但遇到第④个就相对有点模糊了,很多人认为是“B and B”,但事实不是,这里会涉及到多态的两个知识点
1、当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
2、方法调用的优先问题 ,优先级由高到低依次为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)
对于④来说,a2.show(b),类型为A,则this为a2,b是B的一个实例,于是到A里面去找show(B obj)方法,没有找到,于是到A的super(超类)找,A由于没有超类,转到this.show((super)O)优先级查找,this还是为a2,O为B,(super)O即(super)B,翻译一下就是(A)B,其实就是转成了A,所以最后是在A里面去找show(A obj)方法,发现有了这个方法,但这儿还没有完,涉及到上面的知识点1,“被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的”,这里引用变量是A类型,引用对象是B,所以是按照引用对象来确定调用的是谁的方法,就是调用的B类里的show(A obj),但条件是知识点中的“被调用的方法必须是在超类中定义过的”条件要满足才行,因为这里show(A obj) 也是在超类(A)里面被定义过的,所以最终打印的是“B and A”
依照此原理可以用这种方法推断其他答案
]]>封装可以被认为是一种能够保护代码和数据被定义在类外的其它代码任意访问的屏障。访问数据和代码由一个接口严格控制。 封装的主要好处是修改我们实现的代码而又不会破坏其他人使用我们的代码。封装的这个特性使我们的代码具有可维护性、灵活性以及扩展性。 –摘自:维基百科
1、修改属性的可见性来限制属性的访问,这就要得益于java中private、protected、public各种修饰符的访问权限了,例如:
1 | public class Man { |
这段代码中,把name和age用private修饰符设置为私有的,只有本类才能访问,其他的类都访问不了,这样相当于实现了上述摘要里面所说的“数据隐藏”
2、对属性值提供对外的公共访问方法,创建一对赋值、取值方法,作用于访问私有属性,例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class Man {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
采用 this 关键字是为了解决实例变量(private String name)和局部变量(setName(String name)中的name变量)之间发生的同名的冲突。
创建一个Man类
1 | public class Man { |
创建一个Woman类
1 | public class Woman { |
所以封装就是可以把一个对象的属性私有,而提供一些可以被外界访问的方法,以上实例中public方法是外部类访问该类成员变量的入口。
以Man类的name属性为例,他有get和set方法,get即为获取值,set即为赋值,用的public修饰符修饰,可以被外界所访问,在比如说Man的wife属性,就没有ge方法,男人都想金屋藏娇,外界是不能获取到wife属性的
问题继续,上面的例子还无法详细表现出封装的特性,封装的主要好处是修改我们实现的代码而又不会破坏其他人使用我们的代码,用一个例子:
1 | public void setAge(int age) { |
上例中就是一个对封装特性的一个很好的表现,在输入年龄的时候不小心输入200岁的人妖就可以进入到判断里面给人提示出来了,调用者还是使用的Woman.getAge()来调用获取age值的,没有修改任何代码,但我们在在getAge()方法里面可以有一些扩展,不用担心会造成多处更改的情况,相对维护性来说,非常方便
]]>在多个类中存在相同属性和行为时,将这些内容单独抽取到一个类中,那么多个类就不需再次定义这些类和属性,只要继承那个类就行了
在继承的关系里,被继承的类称为父类、超类或者基类,而继承的类就被称为子类。子类继承了父类的所有属性(包括private成员,并不可访问)和方法,通过继承可以对对象描述更加清晰,提高代码的复用性。不仅如此,子类还可以根据自己的独特性,扩展属性和方法。
通过 extends
关键字让类与类之间产生继承关系。语法:public calss 子类名 extends 父类名{}
1 | public calss Cat extends Animal{} |
例:
创建一个Animal(动物)类
1 | public class Animal { |
创建一个Cat(猫)类,然后让Cat类继承自Animal
1 | public class Cat extends Animal { |
这样就实现了继承,Cat就是Animal的子类
就这样实现了一个继承貌似也没有什么特别的意义,继承的意义在于让子类和父类可以产生差异,我们来详细的看看怎样让子类和父类产生差异的,主要有两种方法
直接在子类中添加新的方法和成员变量。这意味着父类的成员变量或方法不能满足子类的需要,因此需要添加新的来满足子类的需求,以上面的Cat为例
1 | public class Cat extends Animal { |
上例方法中猫的技能在Animal类中没有方法能满足,所以添加了一个新的方法满足了需求,既能调用父类的方法,有能调用自己特有的扩展方法
overriding的作用不是要添加父类没有的方法,而是覆盖父类的方法,(前提是父类方法访问权限不是private,且子类方法权限大于等于父类方法),这里会涉及到几个访问权限修饰符的知识点,不懂可以特别研究下
1 | public class Cat extends Animal { |
这里对上述代码有两个点需要进行一下描述:
1、大家也看见了fun2()方法上有一个@Override
,@Override表示下面出现的方法会出现重写操作,由编译器检查,如果达不到重写的条件,就会报错。一定程度上增加了代码的安全性和健壮性,可有可无,为了可读性高,留下更好
2、覆盖的方法必须与父类定义完全相同,注意这里是方法名和返回值及参数都要相等,拿参数来说,如果参数变了,那么这就不能叫覆盖(重写)了,而是“重载”,这里不深入重载的研究了,不过重写的方法修饰符是可以改变的,不过还是得遵循(子类方法权限大于等于父类方法)定律
好,回到上面的例子,这里打印的是Animal fun1 ...
和Cat fun2 ...
,说明fun1()由于继承的关系执行的是父类的方法,如果Cat里面没有fun2()方法的话肯定也会执行父类里面的fun2()方法,但这里由于子类(Cat)覆盖了父类的fun2()方法,所以执行到了Cat.fun2();最终打印Cat fun2 ...
,这就表明overriding成功了
子类构造器中的this的调用 → 父类成员变量初始化 → 父类构造方法 → 子类变量初始化 → 子类构造方法
究其原因,是子类因为继承特性,所以拥有了父类的数据,也就是成员变量,而子类在写这些数据时,必须先知道父类的如何定义或者初始化的
1 | public class Animal { |
可见就算是没有构造方法,创建了Cat对象后,也是先调用的父类方法,后调用子类方法
总结:
1、子类重写父类的方法,必须同父类方法相同
2、子类重写父类的方法,子类方法权限需大于等于父类方法
3、子类继承父类后调用的运行顺序依次是:父类成员变量初始化 → 父类构造方法 → 子类变量初始化 → 子类构造方法