首页> 标签> 编译器
"编译器"
共 15474 条结果
全部 问答 文章 公开课 课程 电子书 技术圈 体验
GraalVM在Facebook大量使用,性能提升显著!
​翻译自​​https://medium.com/graalvm/graalvm-at-facebook-af09338ac519​Facebook正在使用GraalVM来加速其Spark的工作负载,并减少内存和CPU的使用。请继续阅读,了解它们的迁移故事、性能改进结果和未来计划。Facebook背后的技术拥有28亿月活跃用户的Facebook是世界上访问量最大的平台之一。为了保证在这种负载下的可靠性和高性能,工程团队采用了多种技术,包括Java、JavaScript、Flow Hack、PHP、Python、c++等。Facebook在一些关键领域使用了Java,如大数据(Spark、Presto等)、后端服务和移动设备。在迁移到GraalVM之前,该团队在Java 8和Java 11上使用了Oracle JDK和OpenJDK。在这种规模下,任何性能改进都会带来显著的价值——它们改善了用户体验并降低了基础设施成本。这就是为什么工程团队一直在寻找改进应用程序性能的方法,并决定评估GraalVM,以确定它是否是一个更快的Java运行时。为什么是GraalVM由于性能是一个主要考虑因素,Facebook团队决定评估GraalVM作为他们的Java运行环境,看看它是否会提高他们的Java应用程序的性能。GraalVM提供了高级优化,比如部分转义分析和内联启发式。多亏了这一点,许多Java/JVM应用程序只要切换到GraalVM,就能立即获得性能提升。Facebook团队还观察到,与C2相比,GraalVM在SpecJVM2008和DaCapo等基准测试中显示出了显著的年进步。此外,GraalVM编译器是以模块化和可扩展的方式使用Java从头开始编写的。这使得维护变得很容易,同时还增加了增量改进。这对Facebook来说很重要,因为该团队正在考虑对GraalVM进行长期投资。社区。GraalVM项目拥有一个充满活力的开源社区,许多组织和个人都为该项目做出了贡献,并形成了它的路线图。在社区中也很容易找到帮助和支持。在GraalVM上运行Java和SparkFacebook团队使用了GraalVM社区作为OpenJDK的替代品。在这个场景中,迁移到GraalVM非常简单——只需要切换运行环境,不需要更改应用程序代码。这种转换使得应用程序运行得更快,这得益于GraalVM的高级性能优化,无需任何手动调优。Apache Spark是一个统一的大数据处理分析引擎,内置流、SQL、机器学习和图形处理模块。它处理数据的速度非常快,但许多团队正在寻找进一步优化其性能的方法。最简单的方法之一是在GraalVM上运行Spark工作负载。多亏了一组特定的编译器优化(我们稍后将详细讨论),GraalVM可以显著加快Spark的工作负载。Renaissance基准测试套件的Apache Spark基准测试显示,社区的平均加速速度为1.1倍,企业的平均加速速度为1.42倍,有些基准测试的速度高达4.84倍。对于Facebook来说,Spark是其数据仓库中最大的SQL查询引擎,运行在聚合计算存储集群上。由于数据量巨大,效率和成本的降低是当务之急。他们从2020年初开始进行评估。由于最初的基准测试显示了良好的结果,团队将gralvm推向了生产,并一直监控其性能和可靠性。![image.png](https://img-blog.csdnimg.cn/img_convert/a2eb859a2fe03b6653189e15e12f5c04.png#align=left&display=inline&height=381&margin=[object Object]&name=image.png&originHeight=381&originWidth=683&size=39756&status=done&style=none&width=683)在性能方面,他们观察到CPU使用减少了约10%,而且自推出以来,CPU的减少一直保持一致。GraalVM如何加速Spark工作负载对Spark性能提升贡献最大的一些优化是:多态内联。只有当编译器能够确定方法调用的目标方法时,传统内联才能工作。GraalVM通过收集额外的分析信息(允许也内联抽象方法),使内联超越了这一点。部分逸出分析。部分转义分析的思想是通过在对象没有转义的分支中执行标量替换来删除不必要的对象分配,并确保对象存在于必须转义的分支中的堆中。这既减少了应用程序的内存占用,又减少了GC引起的CPU负载。这种优化在Spark这样的数据密集型应用程序中更加重要。特别是,根据Facebook的观察,GraalVM在java/lang/Double.valueOf等方法中减少了5倍的CPU消耗。GraalVM中的高级推测性优化通过利用动态运行时反馈产生更快的机器码。通过推测程序的某些部分不会在程序执行期间运行,GraalVM编译器能够专门化代码并使其更高效。对于Spark,通过消除分支(如长if-then-else链)、简化控制流、减少循环体中的动态检查数量以及建立别名约束,这种优化工作得特别好,从而实现进一步的优化。根据评估结果,Facebook团队将大部分cpu密集型的大数据服务迁移到了GraalVM。他们还观察到,在切换到GraalVM后,Presto的>5%的CPU和GC暂停时间提高了。接下来,该团队计划将GraalVM推到其他内存绑定服务,以从escape分析优化中获益。该团队还计划为项目和社区做出贡献。他们还在探索使用其他gralvm特性的机会,如Native Image和Truffle Framework。结论多亏了高级编译器优化,GraalVM可以显著加快许多Java和Scala工作负载。特别是,通过将GraalVM转换为JDK发行版,Spark的工作负载有望提高10%-42%。有趣的是,另一个流行的社交媒体平台Twitter的工程师也分享了类似的旅程和观察结果。在将Scala的工作负载转移到GraalVM之后,他们观察到显著的性能改进,例如,多亏了GraalVM编译器,P99延迟降低了19.9%。对于像Twitter或Facebook这样的平台,这种性能改进会随着平台规模的扩大而进一步扩大。要开始在您的应用程序中使用GraalVM,请访问graalvm.org/docs/getting-started/。
文章
SQL  ·  分布式计算  ·  Java  ·  大数据  ·  编译器  ·  测试技术  ·  Scala  ·  Apache  ·  Spark  ·  Python
2023-02-02
Anaconda环境配置Python GDAL库
  本文介绍在Anaconda环境下,安装Python中栅格、矢量等地理数据处理库GDAL的方法。  需要注意的是,本文介绍基于conda install命令直接联网安装GDAL库的方法;这一方法有时不太稳定,且速度较慢。因此,如果有需要,大家可以利用基于whl文件的配置方法,更快速地配置GDAL库;这一方法的介绍我们将在后期博客中涉及。  首先,我们打开“Anaconda Prompt (Anaconda)”软件。  随后,将弹出如下所示的命令输入窗口。  在上述弹出的命令输入窗口中,输入以下代码:conda install -c conda-forge gdal  随后,系统将自动搜索GDAL这一模块,并准备安装。  在这里有一点需要注意——也是我们在之前很多Python模块安装教程文章中提到的:如果我们开启了网络代理软件,则可能会导致系统找不到GDAL这一模块的元数据的下载地址,出现如下所示的错误提示。  针对这种情况,我们将网络代理软件关闭后,重新输入前述代码,即可解决问题。  另一方面,在我实际操作的过程中,发现在这一步骤里,配置环境环节进行得会稍微有些慢;但是稍等片刻还是可以正常配置完毕的。  待系统找到GDAL这一模块的元数据后,我们输入y即可开始下载、安装的过程。  成功完成下载与安装后,会出现如下所示的界面。  为了验证我们GDAL模块的安装是否成功,我们可以在编译器中尝试加载这一模块;若发现可以成功加载,则说明GDAL模块安装无误。  至此,大功告成。
文章
编译器  ·  数据处理  ·  Python
2023-02-02
泰山众筹sun4.0开发系统DAPP模式
  智能合约是对协议的翻译,包括将条款和条件转换成计算机代码。区块链开发者用JAVA、C++和其他编程语言编写脚本,不会引起歧义或误解。这段代码翻译了一组自动执行和验证的规则。  双方的合同代码被上传到区块链,以检查合同的有效性并启用所需的步骤。从初始化开始,智能合约将自动执行。智能合约与传统合约的主要区别在于,智能合约不依赖于第三方,加密代码自动执行。  区块链是一个链接数据的结构,它包含数据和指向以前数据的散列指针。通常,事物是相互关联的。每一笔交易都在区块链记录并公布。我们之前看到的属性确保了区块链内交易的安全性。  从区块链到智能合约  智能合约在分布式区块链中定义和执行。每笔交易和合同的执行都必须在区块链环境上进行。实现这种智能合约执行有几个步骤:  1.区块链开发者使用编程语言编写智能合约。在编码部分,开发人员实现契约背后的逻辑,以便当给定的操作或事物发生时,脚本支持以下步骤。  2.智能合约代码编写完成后,脚本将被发送到区块链。分布式网络用于执行代码。正常情况下,每一台可以用于计算的计算机都可以执行契约,对于相同的输入,无论计算机在哪里执行,契约的输出都应该是相同的。  3.可以对多个条件进行编码,最终的智能合约用户可以选择该智能合约所需的条件。  Hyperledger Fabric:在Fabric中,Chaincode是部署在网络上的程序代码,在共识过程中由链验证器一起执行和验证。  NXT:这是一个公共区块链平台,包含有限的智能合约模板选择。你必须使用给定的东西,你不能编写自己的代码。  编写和部署智能合约的工具  Mist Browser——它是一个浏览和使用dApp的工具。它是一个单独的浏览器,可用于浏览dApp并与之交互。  Truffle Framework——Truffle是一个流行的以太坊开发框架。它具有内置的智能合约编译、链接、部署和二进制管理。  Metamask——MetaMask是一座桥梁,允许人们今天在他们的浏览器中访问明天的分布式网络。它允许用户直接在浏览器中运行以太坊dApp,而无需运行完整的以太坊节点。  Remix——Remix是一个基于Web浏览器的IDE,允许用户编写Solidity智能合约,然后部署和运行智能合约。  编写以太坊智能合约的编程语言  Solidity和Serpent是编写以太坊智能合约的两种主要语言。  Solidity:3它是一种面向合约的高级语言,其语法类似于JavaScript,旨在针对以太坊虚拟机(EVM)。  Serpent:Serpent是一种用于编写以太坊合约的高级语言。  尽管Solidity是目前最流行的智能合约语言,但有一些即将推出的智能合约语言在未来可能会变得很重要。  即将到来的编程语言:  Viper:Viper有一个类似Python的缩进方案。它侧重于安全性和语言以及编译器的简单性。  Lisk:Lisk使用javascript作为智能合约语言,这使得开发人员更容易编写应用程序。  Chain:Chain提供企业级区块链基础设施,带有Ruby、Java和NodeJS等流行语言的SDK。  合约的执行是以点对点的方式进行的,非常接近于去中心化。连接到互联网的简单用户通常可以是客户端。他们必须在计算机上安装客户端。我们称这个原理为挖掘。用来运行程序的计算机叫做节点。  通常,每个人都可以创建一个智能合同,并将其上传到区块链的特定交易中。根据所使用的技术,特定的虚拟机将执行该代码。例如,以太坊智能合约在以太坊虚拟机上执行。合同获得资金支持,与传统支付方式一样,根据特定协议,可以使用一些API来公开合同,以便自动执行交易。
文章
JavaScript  ·  前端开发  ·  Java  ·  编译器  ·  区块链  ·  开发工具  ·  数据安全/隐私保护  ·  开发者  ·  C++  ·  Python
2023-02-02
【高效编码】JDK自带的命令行工具的使用还用不清楚的地方?快来看看这篇文章吧!!!
您好,我是码农飞哥,感谢您阅读本文!如果此文对您有所帮助,请毫不犹豫的一键三连吧。小伙伴们有啥想看的,想问的,欢迎积极留言告诉我喔。 上一篇文章我们介绍了IDEA的调试技巧【高效编码】关于IDEA调试的点点滴滴都在此文了。领导看了都说好!!!!。开发工具的熟练使用可以有效提高我们的效率,但是这还不够,还有JDK我们也要熟悉起来。所以这一篇文章来了,它主要是讲述JDK自带的各种命令行工具的使用。也是属于操作类的文章,简单易懂,掌握之后受益良多。总览JDK自带的命令工具都在C:\Program Files\Java\jdk1.8.0_60\bin 这个目录下,即JDK安装目录的bin目录下,这个里面有各种命令行工具,有用来监控虚拟机运行的命令,有用来故障排查的命令,还有一些基础命令。下图是对本文即将介绍了几个工具命令以及它们的作用做的一个汇总表格。一共有6个命令。后面会详细的介绍每个命令的使用。下面就分别详细介绍一下各个命令的使用吧,话不多说,让我们直入主题。准备工作为了测试方便,我们先准备一个用于测试的java源文件(即以.java为后缀的文件),上传到服务器上。上传了一个User.java文件。放在了/data/server/test-xiang 这个目录下,目录没有的话可以自行创建(目录可以不用相同,但需要的是一个新目录,方便后面查看效果)。javac首先粉墨登场的是我们的javac命令,这个命令作用是用于Java语言编译器,即将Java 源文件(.java文件)编译成一个类文件(.class文件)。它的用法是:javac <options> <source files>用法非常简单,其中<options>是各种操作符,<source files> 是源文件。可以通过javac -help命令查看各种操作符的功能描述。如下图所示:举个栗子吧,就拿前面已经上传的User.java文件为例:javac -g /data/server/test-xiang/User.java执行该命令之后,我们看到该目录下生成了一个同名的User.class文件,这个文件就是我们源文件编译之后的类文件,所有的Java源文件必须先编译成类文件,才能被JDK执行。javadoc说完了javac命令之后,我们接下来看看javadoc命令,这个命令的作用就是生成API文档。其用法是:javadoc [options] [packagenames] [sourcefiles] [@files]其中,[options] 表示操作,[packagenames]表示包名,[sourcefiles]表示源文件(即.java文件) 同样的可以通过javadoc -help 查看各个操作的作用描述。举个栗子:显示User.java中所有的public类和成员,生成相关的文档。javadoc -public /data/server/test-xiang/User.java执行该命令之后会生成如下文件:javap接下来介绍是javap 这个命令,这个命令的作用就是类文件反汇编器,主要是根据Java字节码文件反汇编为Java源代码文件。说白了,就是可以按需查看类文件的信息。其用法是:javap <options> <classes><options> 是各种操作,<classes>是类文件, 同样的还是可以通过javap -help查看一下各种操作的解释吧!举个栗子:比如我要显示类文件中所有公共成员(即被public修饰的方法和变量)javap -public /data/server/test-xiang/User.classjar接下来介绍的是jar命令,这个命令的主要作用是创建和管理jar文件。它的用法是:jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...{ctxui} 表示这一部分可以输入 c,t,x,u,i 中的一个。[vfmn0PMe] 表示可以输入vfmn0PMe中的多个,比如输入 vf 表示答应压缩信息,并指定压缩文件名[jar-file] 可以设置jar的文件名。我们可以通过命令jar -help 查看各个选项的功能描述,如下图所示:这里有一个非常清晰的描述, 比如-c 命令就是创建新jar包,-f 就是指定jar的名称比如:将两个类文件归档到一个名为classes.jar的档案中,可以输入如下命令:jar cvf classes.jar Foo.class Bar.class再比如:将文件夹下已经有了User.java 和User.class 两个文件打包到一个jar文件中,那么可以通过jar命令将这两个文件打包jar cvf test.jar User.java User.class执行命令之后,就可以看到目录下已经生成了一个名叫test.jar的文件。在对该文件进行解压看看,可以看到有User.jar文件和User.class文件。是不是非常的简单呀。jar -xvf test.jarjava接着就是介绍Java这个命令了,这个命令非常非常的重要,必须要画五角星重点掌握。这个命令的作用主要有两个。一个是执行类(这里的类是指含有main函数的类),用法是:java [-options] class [args...]一个是执行jar文件,用法是:java [-options] -jar jarfile [args...]当然我们还可以通过java -help命令查看其他的操作命令。举个栗子:直接执行主类,如下有这样一个有main方法的主类,该主类没有包名。public class JavaOptionTest { public static void main(String[] args){ System.out.println("测试java命令的执行"); } }如果我们要想在linux下直接运行该类的话,只需要步:将源文件编译成类文件(.class文件)javac JavaOptionTest.java通过java命令执行main函数java JavaOptionTes我们可以看到控制台打印出来我们期待的结果。那么问题来了,如果一个主类带有包名的话,那么该如何执行呢?首先需要进入包名所在父目录,就像下图中进入D:\workspace\java-base-demo\target\classes目录,然后带包名运行:java com.jay.base.JavaOptionTest在执行类中有的小伙伴可能会碰到类似这样的问题:错误: 找不到或无法加载主类 com.jay.base.JavaOptionTest这是因为在你当前的执行命令的目录下面找不到 com.jay.base这个路径,所以找不到JavaOptionTest类文件执行jar包,如下在target目录下有一个springboot项目的jar直接通过java命令即可启动该项目。java -jar testng-spring-boot-demo-0.0.1-SNAPSHOT.jar延伸阅读直接通过java命令启动jar包的话,有个问题就是当窗口连接关闭之后服务也就停了,即这样启动不是后台启动应用的。要想在后台启动服务还是需要通过nohup命令来启动服务标准的启动命令是:nohup java -jar testng-spring-boot-demo-0.0.1-SNAPSHOT.jar &指定端口,指定运行内存的启动命令是:nohup java -Xms1024m -Xmx2048m -jar testng-spring-boot-demo-0.0.1-SNAPSHOT.jar --serve这个命令表示指定初始堆内存(Xms)为1024mb,最大内存是2048mb,启动端口是9393。这样执行的话会在执行命令的目录下生成一个 nohup.out文件记录启动日志。当然我们也可以指定一个文件记录启动日志,如下命令:nohup java -Xms1024m -Xmx2048m -jar testng-spring-boot-demo-0.0.1-SNAPSHOT.jar --ser就指定生成了log.file文件来记录启动日志。总结本文详细介绍了基础部分的各种命令行工具,作为一个Java程序猿熟练的掌握这些命令的使用还是很有必要的,我觉得这里最最重要的命令是java命令。希望本文对读者朋友们有所帮助。
文章
运维  ·  监控  ·  Java  ·  编译器  ·  Linux  ·  程序员  ·  API  ·  开发工具
2023-02-02
千姿百态,瞬息万变,Win11系统NeoVim打造全能/全栈编辑器(前端/Css/Js/Vue/Golang/Ruby/ChatGpt)
我曾经多次向人推荐Vim,其热情程度有些类似现在卖保险的,有的时候,人们会因为一些弥足珍贵的美好暗暗渴望一个巨大的负面,比如因为想重温手动挡的快乐而渴望买下一辆二十万公里的老爷车,比如因为所谓完美的音质而舍不得一个老旧的有线耳机,比如因为一个铜炉火锅而期待北京那漫长而寒冷的冬天。 也许有的人会因为Vim而放弃169刀的JetBrains全家桶,没错,Vim的快乐,就是手动挡的快乐,懂得自然懂,不懂的永远也不会懂,但如果没有用Vim敲过代码,那么绝对枉生于有Vim的世界。之前一篇:上古神兵,先天至宝,Win11平台安装和配置NeoVim0.8.2编辑器搭建Python3开发环境(2023最新攻略),我们已经配置好了Python3开发环境,本次继续添砖加瓦,让NeoVim进化为全栈编辑器,全知全能,无所不通。全能补全:coc.nvim之前配置Python补全,我们使用过NCM2扩展插件:Plug 'ncm2/ncm2' Plug 'roxma/nvim-yarp' Plug 'ncm2/ncm2-bufword' Plug 'ncm2/ncm2-path' Plug 'ncm2/ncm2-jedi'五个插件,仅仅为了Python的补全,而Coc.nvim 通过 Microsoft 的 Language Server Protocol,支持许多编程语言,包括 JavaScript, Python, C++ ,Ruby等等。同时还可以通过设置和扩展进行灵活定制,满足不同用户的需求。 重新编写配置:Plug 'neoclide/coc.nvim', {'branch': 'release'}安装插件::PlugInstall安装Python补全::CocInstall coc-pyls就这么简单。随后,还可以对其他目标语言进行设置,比如想支持Golang的补全,通过命令::CocConfig打开配置文件,Win11默认路径是:~\AppData\Local\nvim\coc-settings.json{ "languageserver": { "golang": { "command": "gopls", "rootPatterns": [ "go.mod" ], "filetypes": [ "go" ] } }, "suggest.noselect": false, "coc.preferences.diagnostic.displayByAle": true, "suggest.floatEnable": true }添加Golang的配置,这里使用gopls模块。 正确配置之后,就可以使用代码补全了 例如我们输入 fmt. 就会提示fmt包中的方法,默认选择第一个,使用< C-n > < C-p > 上下选择,回车确认,nvim下可以使用悬浮窗功能。 类似的,如果想配置Ruby的智能提示,设置不需要配置文件,只需要安装对应模块即可:gem install solargraph随后NeoVim内运行命令::CocInstall coc-solargraph但这也带来了一个问题,即编译运行的时候,默认运行的语言是Python,如何让Vim程序自动进行判断?只需要修改配置即可:autocmd FileType python nnoremap <C-B> :sp <CR> :term python % <CR> autocmd FileType go nnoremap <C-B> :sp <CR> :term go run % <CR> nnoremap <C-W> :bd!<CR>这里通过NeoVim中的autocmd进行判断,如果是Python代码就通过python解释器运行,如果是golang代码就通过Golang的编译器进行编译,互不影响。 NeoVim 的 autocmd 是用来自动执行命令的一种机制。它可以在特定的事件发生时触发命令的执行,比如打开文件、保存文件等。这样可以自动地对文件进行格式化、添加头部信息等操作。 前端的补全更简单,一键式命令安装即可::CocInstall coc-vetur coc-json coc-html coc-css但前端页面默认是没有闭合高亮的,所以推荐下面这个插件:Plug 'leafOfTree/vim-matchtag'它可以针对前端页面标签的闭合进行动态高亮: 非常方便。快捷操作与配置也许有人会因为诸如保存、注释以及记录等操作还需要输入vim命令而苦恼,但其实这并不是什么问题,Vim也可以自动保存:Plug 'Pocco81/auto-save.nvim'这样就可以免去:w的操作。单行以及多行的批量注释可以依赖这个插件:Plug 'tpope/vim-commentary'这样就可以通过组合键gc快速进行注释操作了。 编辑操作记录可以依赖这个插件:Plug 'mhinz/vim-startify'如此可以在首页动态的选择曾经编辑过的文件: 想要传统IDE那样的动态调节字体大小?let s:fontsize = 12 function! AdjustFontSize(amount) let s:fontsize = s:fontsize+a:amount :execute "GuiFont! Consolas:h" . s:fontsize endfunction inoremap <expr> <TAB> pumvisible() ? "\<C-y>" : "\<CR>" inoremap <expr> <Esc> pumvisible() ? "\<C-e>" : "\<Esc>" inoremap <expr> <C-j> pumvisible() ? "\<C-n>" : "\<Down>" inoremap <expr> <C-k> pumvisible() ? "\<C-p>" : "\<Up>"通过tab键选择自动补全的代码提示?" In insert mode, pressing ctrl + numpad's+ increases the font inoremap <C-kPlus> <Esc>:call AdjustFontSize(1)<CR>a inoremap <C-kMinus> <Esc>:call AdjustFontSize(-1)<CR>a在Vim中,你甚至可以和ChatGpt一亲芳泽:use({ 'terror/chatgpt.nvim', run = 'pip3 install -r requirements.txt' })当然,在用户目录下需要chatgpt的apikey或者token: ~/.chatgpt-nvim.json:{ "authorization": "<API-KEY>", # Optional API key "session_token": "<SESSION-TOKEN>" # Your ChatGPT session token }由于api-key是收费的,这里建议使用token: 访问 https://chat.openai.com/chat 并且登录 按F12打开开发者工具 在应用的标签上 > 选择Cookies 直接复制\_\_Secure-next-auth.session-token的value值写到上面的session\_token中即可。效果如下: 最后,完整的全栈NeoVim配置:call plug#begin('C:\nvim-win64\nvim-win64\share\nvim\plugged') Plug 'navarasu/onedark.nvim' Plug 'pablopunk/native-sidebar.vim' Plug 'Pocco81/auto-save.nvim' Plug 'leafOfTree/vim-matchtag' Plug 'mhinz/vim-startify' Plug 'neoclide/coc.nvim', {'branch': 'release'} Plug 'tpope/vim-commentary' call plug#end() let g:onedark_config = { \ 'style': 'warm', \} colorscheme onedark let g:native_sidebar_shortcut = '<c-t>' set clipboard^=unnamed,unnamedplus syntax on "syntax highlighting, see :help syntax filetype plugin indent on "file type detection, see :help filetype set number "display line number set path+=** "improves searching, see :help path set noswapfile "disable use of swap files set wildmenu "completion menu set backspace=indent,eol,start "ensure proper backspace functionality set undodir=~/.cache/nvim/undo "undo ability will persist after exiting file set undofile "see :help undodir and :help undofile set incsearch "see results while search is being typed, see :help incsearch set smartindent "auto indent on new lines, see :help smartindent set ic "ignore case when searching set expandtab "expanding tab to spaces set tabstop=4 "setting tab to 4 columns set shiftwidth=4 "setting tab to 4 columns set softtabstop=4 "setting tab to 4 columns set showmatch "display matching bracket or parenthesis set hlsearch incsearch "highlight all pervious search pattern with incsearch highlight ColorColumn ctermbg=9 "display ugly bright red bar at color column number " Keybind Ctrl+l to clear search nnoremap <C-l> :nohl<CR><C-l>:echo "Search Cleared"<CR> " When python filetype is detected, F5 can be used to execute script " autocmd FileType python nnoremap <buffer> <c-b> :<cr>:exec '!python' shellescape(expand('%:p'), 1)<cr> autocmd FileType python nnoremap <C-B> :sp <CR> :term python % <CR> autocmd FileType go nnoremap <C-B> :sp <CR> :term go run % <CR> nnoremap <C-W> :bd!<CR> let s:fontsize = 12 function! AdjustFontSize(amount) let s:fontsize = s:fontsize+a:amount :execute "GuiFont! Consolas:h" . s:fontsize endfunction inoremap <expr> <TAB> pumvisible() ? "\<C-y>" : "\<CR>" inoremap <expr> <Esc> pumvisible() ? "\<C-e>" : "\<Esc>" inoremap <expr> <C-j> pumvisible() ? "\<C-n>" : "\<Down>" inoremap <expr> <C-k> pumvisible() ? "\<C-p>" : "\<Up>" " In insert mode, pressing ctrl + numpad's+ increases the font inoremap <C-kPlus> <Esc>:call AdjustFontSize(1)<CR>a inoremap <C-kMinus> <Esc>:call AdjustFontSize(-1)<CR>a只需要不到70行的配置,我们就拥有了一个万能的Vim编辑器。结语满打满算,七个插件,全知全能,而我们需要做的,只是一行简单的:PlugInstall。因为什么?因为热爱,如果是真爱,哪怕风情万千遇到不解风情,也所甘愿,哪怕没人懂,也要周周至至做出来。
文章
前端开发  ·  JavaScript  ·  IDE  ·  编译器  ·  Go  ·  开发工具  ·  C++  ·  开发者  ·  Ruby  ·  Python
2023-02-02
Java基础知识面试题
1.1 Java概述1.1.1 何为编程编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程。为了使计算机能够理解人的意图,人类就必须要将需解决的问题的思路、方法、和手段通过计算机能够理解的形式告诉计算机,使得计算机能够根据人的指令一步一步去工作,完成某种特定的任务。这种人和计算机之间交流的过程就是编程。1.1.2 什么是JavaJava是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。除此之外,还有目前比较流行的Python、Go等语言,适用于不同的处理场景。1.1.3 jdk1.5之后的三大版本Java SE(J2SE,Java 2 Platform Standard Edition,标准版)Java SE 以前称为 J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为Java EE和Java ME提供基础。Java EE(J2EE,Java 2 Platform Enterprise Edition,企业版)Java EE 以前称为 J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端Java 应用程序。Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web2.0应用程序。2018年2月,Eclipse 宣布正式将 JavaEE 更名为 JakartaEEJava ME(J2ME,Java 2 Platform Micro Edition,微型版)Java ME 以前称为 J2ME。Java ME(J2ME,Java 2 Platform Micro Edition,微型版)Java ME 以前称为 J2ME。Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。基于 Java ME 规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。1.1.4 JVM、JRE和JDK的关系JVMJava Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。JREJava Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。JDKJava Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等**JDK>JRE>JVM** JDK>JRE>JVMJVM&JRE&JDK关系图1.1.5 什么是JAVA跨平台性,其原理是什么所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。1.1.6 Java语言有哪些特点简单易学(Java语言的语法与C语言和C++语言很接近)面向对象(封装,继承,多态)平台无关性(Java虚拟机实现平台无关性)支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的)支持多线程(多线程机制使应用程序在同一时间并行执行多项任)健壮性(Java语言的强类型机制、异常处理、垃圾的自动收集等)安全性1.1.7 什么是字节码?采用字节码的最大好处是什么字节码:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。采用字节码的好处:Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。先看下java中的编译器和解释器:Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。​1.1.8 什么是java程序的主类?应用程序和小程序的主类有何不同?一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中,这个主类是指包含main()方法的类。而在Java小程序中,这个主类是一个继承自系统类JApplet或Applet的子类。应用程序的主类不一定要求是public类,但小程序的主类要求必须是public类。主类是Java程序执行的入口点。1.1.9 java应用程序和小程序之间的差别简单说应用程序是从主线程启动(也就是main()方法)。applet小程序没有main方法,主要是嵌在浏览器页面上运行(调用init()线程或者run()来启动),嵌入浏览器这点跟flash的小游戏类似。1.1.10 java与C++的区别我知道很多人没学过C++,但是面试官就是没事喜欢拿咱们Java和C++比呀!没办法!!!就算没学过C++,也要记下来!都是面向对象的语言,都支持封装、继承和多态Java不提供指针来直接访问内存,程序内存更加安全Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。Java有自动内存管理机制,不需要程序员手动释放无用内存1.1.11 Oracle jDK 和OpenJdk的对比(1)Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一次;(2)OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的;(3)Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题;(4)在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能;(5)Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本;(6)Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。1.2 Java基础语法1.2.1 数据类型Java有哪些数据类型定义:Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。分类基本数据类型数值型整数类型(byte,short,int,long)浮点类型(float,double)字符型(char)布尔型(boolean)引用数据类型类(class)接口(interface)数组([])Java基本数据类型图switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。用最有效率的方法计算 2 乘以 82 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)。Math.round(11.5) 等于多少?Math.round(-11.5)等于多少Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。float f=3.4;是否正确不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 float f =3.4F;。short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换。装箱和拆箱自动装箱是 Java 编译器在基本数据类型和对应的对象包装类型之间做的一个转化。比 如:把 int 转化成 Integer,double 转化成 Double,等等。反之就是自动拆箱。 原始类型: boolean,char,byte,short,int,long,float,double 封装类型:Boolean,Character,Byte,Short,Integer,Long,Float,DoubleString 转出 int 型,判断能不能转?如何转?答:可以转,得处理异常 Integer.parseInt(s) 主要为 NumberFormatException:1)当 你输入为字母时,也就是内容不是数字时,如 abcd2)当你输入为空时3)当你输入超出 int 上限时 Long.parseLong(“123”)转换为 long1.2.2 java 基本类型与引用类型的区别基本类型保存原始值,引用类型保存的是引用值(引用值就是指对象在堆中所 处的位置/地址)int和Integer的区别1、Integer是int的包装类,int则是java的一种基本数据类型2、Integer变量必须实例化后才能使用,而int变量不需要3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值4、Integer的默认值是null,int的默认值是0延伸:关于Integer和int的比较1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。Integer i = new Integer(100); Integer j = new Integer(100); System.out.print(i == j); //false2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)Integer i = new Integer(100); int j = 100; System.out.print(i == j); //true3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为 ①当变量值在-128~127之间时,非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同;②当变量值不在-128~127之间时,非new生成Integer变量时,java API中最终会按照new Integer(i)进行处理(参考下面第4条),最终两个Interger的地址同样是不相同的) Integer i = new Integer(100); Integer j = 100; System.out.print(i == j); //false4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为falseInteger i = 100; Integer j = 100; System.out.print(i == j); //true Integer i = 128; Integer j = 128; System.out.print(i == j); //false对于第4条的原因:java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下:public static Integer valueOf(int i){ assert IntegerCache.high >= 127; if (i >= IntegerCache.low && i <= IntegerCache.high){ return IntegerCache.cache[i + (-IntegerCache.low)]; } return new Integer(i); }java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了1.2.3 编码及注释Java语言采用何种编码方案?有何特点?Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。定义:用于解释说明程序的文字分类单行注释格式: // 注释文字多行注释格式: /* 注释文字 /文档注释格式:/* 注释文字 */作用在程序中,尤其是复杂的程序中,适当地加入注释可以增加程序的可读性,有利于程序的修改、调试和交流。注释的内容在程序编译的时候会被忽视,不会产生目标代码,注释的部分不会对程序的执行结果产生任何影响。注意事项:多行和文档注释都不能嵌套使用。1.2.4 访问修饰符访问修饰符 public,private,protected,以及不写(默认)时的区别定义:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。分类private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。public : 对所有类可见。使用对象:类、接口、变量、方法访问修饰符图1.2.5 运算符&和&&的区别&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。。注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。1.2.6 关键字Java 有没有 gotogoto 是 Java 中的保留字,在目前版本的 Java 中没有使用。final 有什么用?用于修饰类、属性和方法;被final修饰的类不可以被继承被final修饰的方法不可以被重写被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的Final、 finally 、finalize区别final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。this关键字的用法this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。this的用法在java中大体可以分为3种:1.普通的直接引用,this相当于是指向当前对象本身。2.形参与成员名字重名,用this来区分:public Person(String name, int age) { this.name = name; this.age = age; }3.引用本类的构造函数class Person{ private String name; private int age; public Person() { } public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); this.age = age; } }super关键字的用法super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。super也有三种用法:1.普通的直接引用与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分1 class Person{ 2. protected String name; 3. 4. public Person(String name) { 5. this.name = name; 6. } 7. 8. } 9. 10. class Student extends Person{ 11. private String name; 12. 13. public Student(String name, String name1) { 14. super(name); 15. this.name = name1; 16. } 17. 18. public void getInfo(){ 19. System.out.println(this.name); //Child 20. System.out.println(super.name); //Father 21. } 22. 23. } 24. 25. public class Test { 26. public static void main(String[] args) { 27. Student s1 = new Student("Father","Child"); 28. s1.getInfo(); 29. 30. } 31. }3、引用父类构造函数super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。this与super的区别super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。super()和this()均需放在构造方法内第一行。尽管可以用this调用一个构造器,但却不能调用两个。this和super不能同时出现在一个构造函数里面,因为this必然会调 用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。static存在的主要意义static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法!static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。static的独特之处1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩?2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。static应用场景因为static是被类的实例对象所共享,因此如果某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量。因此比较常见的static应用场景有:1、修饰成员变量 2、修饰成员方法 3、静态代码块 4、修饰类【只能修饰内部类也就是静态内部类】 5、静态导包static注意事项1、静态只能访问静态。 2、非静态既可以访问非静态的,也可以访问静态的。1.2.7 流程控制语句break ,continue ,return 的区别及作用break 跳出总上一层循环,不再执行循环(结束当前的循环体)continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)在 Java 中,如何跳出当前的多重嵌套循环在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如:public static void main(String[] args) { ok: for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.println("i=" + i + ",j=" + j); if (j == 5) { break ok; } } } }1.3 面向对象1.3.1 面向对象概述面向对象和面向过程的区别面向过程:优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。缺点:没有面向对象易维护、易复用、易扩展面向对象:优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护缺点:性能比面向过程低面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。1.3.2 面向对象三大特性面向对象的特征主要有以下几个方面:抽象、封装、继承。抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。封装:封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。关于继承如下 3 点请记住:1.子类拥有父类非 private 的属性和方法。2.子类可以拥有自己属性和方法,即子类可以对父类进行扩展。3.子类可以用自己的方式实现父类的方法。(以后介绍)。多态所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。其中Java 面向对象编程三大特性:封装 继承 多态封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承可以提高代码复用性。继承是多态的前提。关于继承如下 3 点请记住:1.子类拥有父类非 private 的属性和方法。 2子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 3子类可以用自己的方式实现父类的方法。多态性:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:方法重写(子类继承父类并重写父类中已有的或抽象的方法);对象造型(用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。什么是多态机制?Java语言是如何实现多态的?所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。多态的实现Java实现多态有三个必要条件:继承、重写、向上转型。继承:在多态中必须存在有继承关系的子类和父类。重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。面向对象五大基本原则是什么(可选)1 单一职责原则SRP(Single Responsibility Principle)类的功能要单一,不能包罗万象,跟杂货铺似的。2 开放封闭原则OCP(Open-Close Principle)一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。3 里式替换原则LSP(the Liskov Substitution Principle LSP)子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~4 依赖倒置原则DIP(the Dependency Inversion Principle DIP)高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。5 接口分离原则ISP(the Interface Segregation Principle ISP)设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。Java创建对象的方式1、使用new关键字:比如:Student student = new Student();2、使用Class类的newInstance方法:可以使用Class类的newInstance方法创建对象,这个newInstance方法调用无参的构造器创建对象,如:Student student2 = (Student)Class.forName(“根路径.Student”).newInstance(); 或者:Student stu = Student.class.newInstance();3、使用Constructor类的newInstance方法:本方法和Class类的newInstance方法很像,java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象。我们可以通过这个newInstance方法调用有参数的和私有的构造函数。如: Constructor constructor = Student.class.getInstance(); Student stu = constructor.newInstance(); 这两种newInstance的方法就是大家所说的反射,事实上Class的newInstance方法内部调用Constructor的newInstance方法。这也是众多框架Spring、Hibernate、Struts等使用后者的原因。4、使用Clone的方法:无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面的对象的内容全部拷贝进去,用clone方法创建对象并不会调用任何构造函数。要使用clone方法,我们必须先实现Cloneable接口并实现其定义的clone方法。如:Student stu2 = stu.clone();这也是原型模式的应用。5、使用反序列化:序列化和反序列化一个对象,JVM会给我们创建一个单独的对象,在反序列化时,JVM创建对象并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。如:ObjectInputStream in = new ObjectInputStream (new FileInputStream(“data.obj”)); Student stu3 = (Student)in.readObject();1.3.3 类与接口抽象类和接口的对比抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。相同点接口和抽象类都不能实例化都位于继承的顶端,用于被其他实现或继承都包含抽象方法,其子类都必须覆写这些抽象方法不同点备注:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:1.行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。2.选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。普通类和抽象类有哪些区别?普通类不能包含抽象方法,抽象类可以包含抽象方法。抽象类不能直接实例化,普通类可以直接实例化。抽象类能使用 final 修饰吗?答:不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类创建一个对象用什么关键字?对象实例与对象引用有何不同?new关键字,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)1.3.4 变量与方法成员变量与局部变量的区别有哪些变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。从本质上讲,变量其实是内存中的一小块区域成员变量:方法外部,类内部定义的变量局部变量:类的方法中的变量。成员变量和局部变量的区别作用域成员变量:针对整个类有效。局部变量:只在某个范围内有效。(一般指的就是方法,语句体内)存储位置成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。生命周期成员变量:随着对象的创建而存在,随着对象的消失而消失局部变量:当方法调用完,或者语句结束后,就自动释放。初始值成员变量:有默认初始值。局部变量:没有默认初始值,使用前必须赋值。使用原则在使用变量时需要遵循的原则为:就近原则首先在局部范围找,有就使用;接着在成员位置找。在Java中定义一个不做事且没有参数的构造方法的作用Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?帮助子类做初始化工作。一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么?主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。构造方法有哪些特性?名字与类名相同;没有返回值,但不能用void声明构造函数;生成类的对象时自动执行,无需调用。静态变量和实例变量区别静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。静态变量与普通变量区别static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。静态方法和实例方法有何不同?静态方法和实例方法的区别主要体现在两个方面:在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制在一个静态方法内调用一个非静态成员为什么是非法的?由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。什么是方法的返回值?返回值的作用是什么?方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作!1.3.5 内部类什么是内部类?在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。内部类的分类有哪些内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类。静态内部类定义在类内部的静态类,就是静态内部类。 public class Outer { private static int radius = 1; static class StaticInner { public void visit() { System.out.println("visit outer static variable:" + radius); } } }静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,new 外部类.静态内部类(),如下:Outer.StaticInner inner = new Outer.StaticInner(); inner.visit();成员内部类定义在类内部,成员位置上的非静态类,就是成员内部类。public class Outer { private static int radius = 1; private int count =2; class Inner { public void visit() { System.out.println("visit outer static variable:" + radius); System.out.println("visit outer variable:" + count); } } }成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式外部类实例.new 内部类(),如下:Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); inner.visit();局部内部类定义在方法中的内部类,就是局部内部类public class Outer { private int out_a = 1; private static int STATIC_b = 2; public void testFunctionClass(){ int inner_c =3; class Inner { private void fun(){ System.out.println(out_a); System.out.println(STATIC_b); System.out.println(inner_c); } } Inner inner = new Inner(); inner.fun(); } public static void testStaticFunctionClass(){ int d =3; class Inner { private void fun(){ // System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量 System.out.println(STATIC_b); System.out.println(d); } } Inner inner = new Inner(); inner.fun(); } }定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,new 内部类(),如下:public static void testStaticFunctionClass(){ class Inner { } Inner inner = new Inner(); }匿名内部类匿名内部类就是没有名字的内部类,日常开发中使用的比较多。public class Outer { private void test(final int i) { new Service() { public void method() { for (int j = 0; j < i; j++) { System.out.println("匿名内部类" ); } } }.method(); } } //匿名内部类必须继承或实现一个已有的接口 interface Service{ void method(); }除了没有名字,匿名内部类还有以下特点:匿名内部类必须继承一个抽象类或者实现一个接口。匿名内部类不能定义任何静态成员和静态方法。当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。匿名内部类创建方式:​new 类/接口{ //匿名内部类实现部分 }内部类的优点我们为什么要使用内部类呢?因为它有以下优点:1一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据!2内部类不为同一包的其他类所见,具有很好的封装性;3内部类有效实现了“多重继承”,优化 java 单继承的缺陷。4匿名内部类可以很方便的定义回调。内部类有哪些应用场景一些多算法场合解决一些非面向对象的语句块。适当使用内部类,使得代码更加灵活和富有扩展性。当某个类除了它的外部类,不再被其他的类使用时。局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?​局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?先看这段代码:public class Outer { void outMethod(){ final int a =10; class Inner { void innerMethod(){ System.out.println(a); } } } }以上例子,为什么要加final呢?是因为生命周期不一致, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。内部类相关,看程序说出运行结果public class Outer { private int age = 12; class Inner { private int age = 13; public void print() { int age = 14; System.out.println("局部变量:" + age); System.out.println("内部类变量:" + this.age); System.out.println("外部类变量:" + Outer.this.age); } } public static void main(String[] args) { Outer.Inner in = new Outer().new Inner(); in.print(); } }运行结果:局部变量:14内部类变量:13外部类变量:121.3.6 重写与重载、多态构造器(constructor)是否可被重写(override)?答:构造器不能被继承,因此不能被重写,但可以被重载。Java 中是否可以覆盖(override)一个 private 或者是 static 的方法?答:Java 中 static 方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而 static 方法 是编译时静态绑定的。static 方法跟类的任何实例都不相关,所以概念上不适用。 java 中也不可以覆盖 private 的方法,因为 private 修饰的变量和方法只能在当前类中使用, 如果是其他的类继承当前类是不能访问到 private 变量或方法的,当然也不能覆盖。重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?答:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分,即:1.可以在一个类中也可以在继承关系的类中;2.名相同;3.参数列表不同(个数,顺序,类型) 和方法的返回值类型无关。重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。即:1.不能存在同一个类中,在继承或实现关系的类中;2. 名相同,参数列表相同,方法返回值相同,3.子类方法的访问修饰符要大于父类的。4.子类的检查异常类型要小于父类的检查异常多态在实际中的应用:答:在实际开发工作中,常常遇到一个功能有多种实现方式,比如支付方式,有分微信支付、京东支付、支付宝、银联等支付方式,不同支付方式的大概流程大抵相似,实现细节有所区别。这个时候就可以用到java的多态机制,先定义一个公共接口,接口定义支付流程的各个方法,具体的支付方式实现该接口的方法。在控制层,利用spring的注入获取支付类型和支付方式实现类的引用映射,根据请求需要的支付类型就可以调用对应支付方式的方法,以此实现业务的解耦和拓展。后期需要增加支付方式,只需要实现共同接口即可。1.3.7 对象相等判断==和 equals 的区别是什么== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址。equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“== ” 比较这两个对象。情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。举个例子:public class test1 { public static void main(String[] args) { String a = new String("ab"); // a 为一个引用 String b = new String("ab"); // b为另一个引用,对象的内容一样 String aa = "ab"; // 放在常量池中 String bb = "ab"; // 从常量池中查找 if (aa == bb) // true System.out.println("aa==bb"); if (a == b) // false,非同一对象 System.out.println("a==b"); if (a.equals(b)) // true System.out.println("aEQb"); if (42 == 42.0) { // true System.out.println("true"); } } }说明:String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。hashCode 与 equals (重要)​HashSet如何检查重复两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?hashCode和equals方法的关系面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”hashCode()介绍hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)为什么要有 hashCode我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。hashCode()与equals()的相关规定如果两个对象相等,则hashcode一定也是相同的两个对象相等,对两个对象分别调用equals方法都返回true两个对象有相同的hashcode值,它们也不一定是相等的因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)对象的相等与指向他们的引用相等,两者有什么不同?对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否相等。1.3.8 值传递当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递​答:是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的为什么 Java 中只有值传递首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。下面通过 3 个例子来给大家说明example 1 public static void main(String[] args) { int num1 = 10; int num2 = 20; swap(num1, num2); System.out.println("num1 = " + num1); System.out.println("num2 = " + num2); } public static void swap(int a, int b) { int temp = a; a = b; b = temp; System.out.println("a = " + a); System.out.println("b = " + b); }结果: a = 20 b = 10 num1 = 10 num2 = 20解析:在swap方法中,a、b的值进行交换,并不会影响到 num1、num2。因为,a、b中的值,只是从 num1、num2 的复制过来的。也就是说,a、b相当于num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.example 2 public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 }; System.out.println(arr[0]); change(arr); System.out.println(arr[0]); } public static void change(int[] array) { // 将数组的第一个元素变为0 array[0] = 0; }结果: 1 0解析:array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的时同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。很多程序设计语言(特别是,C++和Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为Java程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。example 3 public class Test { public static void main(String[] args) { // TODO Auto-generated method stub Student s1 = new Student("小张"); Student s2 = new Student("小李"); Test.swap(s1, s2); System.out.println("s1:" + s1.getName()); System.out.println("s2:" + s2.getName()); } public static void swap(Student x, Student y) { Student temp = x; x = y; y = temp; System.out.println("x:" + x.getName()); System.out.println("y:" + y.getName()); } }结果: x:小李 y:小张 s1:小张 s2:小李解析:交换之前:交换之后:通过上面两张图可以很清晰的看出: 方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝总结Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。下面再总结一下Java中方法参数的使用情况:一个方法不能修改一个基本数据类型的参数(即数值型或布尔型》一个方法可以改变一个对象参数的状态。一个方法不能让对象参数引用一个新的对象。值传递和引用传递有什么区别值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。1.3.9 Java包JDK 中常用的包有哪些java.lang:这个是系统的基础类;java.io:这里面是所有输入输出有关的类,比如文件操作等;java.nio:为了完善 io 包中的功能,提高 io 包中性能而写的一个新包;java.net:这里面是与网络有关的类;java.util:这个是系统辅助类,特别是集合类;java.sql:这个是数据库操作的类。import java和javax有什么区别刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来说使用。然而随着时间的推移,javax 逐渐的扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包将是太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。所以,实际上java和javax没有区别。这都是一个名字。1.4 IO流1.4.1 Java中 IO 流分为几种?按照流的流向分,可以分为输入流和输出流;按照操作单元划分,可以划分为字节流和字符流;按照流的角色划分为节点流和处理流。Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。按操作方式分类结构图:按操作对象分类结构图:1.4.2 BIO,NIO,AIO 有什么区别?简答BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。详细回答BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。1.4.3 Files的常用方法都有哪些?Files. exists():检测文件路径是否存在。Files. createFile():创建文件。Files. createDirectory():创建文件夹。Files. delete():删除一个文件或目录。Files. copy():复制文件。Files. move():移动文件。Files. size():查看文件个数。Files. read():读取文件。Files. write():写入文件。1.5 反射1.5.1 什么是反射机制JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。静态编译和动态编译**静态编译:**在编译时确定类型,绑定对象**动态编译:**运行时确定类型,绑定对象1.5.2 反射机制优缺点• 优点: 运行期类型的判断,动态加载类,提高代码灵活度。• 缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。1.5.3 反射机制的应用场景反射是框架设计的灵魂。在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1)将程序内所有 XML 或 Properties 配置文件加载入内存中;2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;3)使用反射机制,根据这个字符串获得某个类的Class实例;4)动态配置实例的属性1.5.4 Java获取反射的三种方法1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制public class Student { private int id; String name; protected boolean sex; public float score; } public class Get { //获取反射机制三种方式 public static void main(String[] args) throws ClassNotFoundException { //方式一(通过建立对象) Student stu = new Student(); Class classobj1 = stu.getClass(); System.out.println(classobj1.getName()); //方式二(所在通过路径-相对路径) Class classobj2 = Class.forName("fanshe.Student"); System.out.println(classobj2.getName()); //方式三(通过类名) Class classobj3 = Student.class; System.out.println(classobj3.getName()); } }反射机制详细介绍包括:获取Class对象的方式、获取成员变量、获取构造方法等等。参考:https://blog.csdn.net/qq_43061290/article/details/1050644531.6 网络编程网络编程的面试题重学TCP/IP协议和三次握手四次挥手,包括TCP/IP协议和三次握手四次挥手的知识,计算机网络体系结构,HTTP协议,get请求和post请求区别,session和cookie的区别等。欢迎大家阅读第14章:https://blog.csdn.net/qq_43061290/article/details/1240414201.7 常用API1.7.1 String 相关:StringBuffer、StringBuilde字符型常量和字符串常量的区别形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)占内存大小 字符常量只占两个字节 字符串常量占若干个字节(至少一个字符结束标志)什么是字符串常量池?字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。String 是最基本的数据类型吗答:不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。这是很基础的东西,但是很多初学者却容易忽视,Java 的 8 种基本数据类型中不包括 String,基本数据类型中用来描述文本数据的是 char,但是它只能表示单个字符,比如 ‘a’,‘好’ 之类的,如果要描述一段文本,就需要用多个 char 类型的变量,也就是一个 char 类型数组,比如“你好” 就是长度为2的数组 char[] chars = {‘你’,‘好’};但是使用数组过于麻烦,所以就有了 String,String 底层就是一个 char 类型的数组,只是使用的时候开发者不需要直接操作底层数组,用更加简便的方式即可完成对字符串的使用。String有哪些特性1、不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。2、常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。3、final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。String为什么是不可变的吗?简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下图所以:/** The value is used for character storage. */ private final char value[];String真的是不可变的吗?我觉得如果别人问这个问题的话,回答不可变就可以了。 下面只是给大家看两个有代表性的例子:1)String不可变但不代表引用不可以变 String str = "Hello"; str = str + " World"; System.out.println("str=" + str); 结果: str=Hello World解析:实际上,原来String的内容是不变的,只是str由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。2.通过反射是可以修改所谓的“不可变”对象// 创建字符串"Hello World", 并赋给引用s String s = "Hello World"; System.out.println("s = " + s); // Hello World // 获取String类中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); // 改变value属性的访问权限 valueFieldOfString.setAccessible(true); // 获取s对象上的value属性的值 char[] value = (char[]) valueFieldOfString.get(s); // 改变value所引用的数组中的第5个字符 value[5] = '_'; System.out.println("s = " + s); // Hello_World 结果: s = Hello World s = Hello_World解析:用反射可以访问私有成员, 然后反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。是否可以继承 String 类答:String 类是 final 类,不可以被继承。String str="i"与 String str=new String(“i”)一样吗?​答:不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。String s = new String(“xyz”);创建了几个字符串对象两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。String str1 = "hello"; //str1指向静态区 String str2 = new String("hello"); //str2指向堆上的对象 String str3 = "hello"; String str4 = new String("hello"); System.out.println(str1.equals(str2)); //true System.out.println(str2.equals(str4)); //true System.out.println(str1 == str3); //true System.out.println(str1 == str2); //false System.out.println(str2 == str4); //false System.out.println(str2 == "hello"); //false str2 = str1; System.out.println(str2 == "hello"); //true如何将字符串反转?答:使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。示例代码: // StringBuffer reverse StringBuffer stringBuffer = new StringBuffer(); stringBuffer. append("abcdefg"); System. out. println(stringBuffer. reverse()); // gfedcba // StringBuilder reverse StringBuilder stringBuilder = new StringBuilder(); stringBuilder. append("abcdefg"); System. out. println(stringBuilder. reverse()); // gfedcba数组有没有 length()方法?String 有没有 length()方法数组没有 length()方法 ,有 length 的属性。String 有 length()方法。JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。String 类的常用方法都有那些?indexOf():返回指定字符的索引。charAt():返回指定索引处的字符。replace():字符串替换。trim():去除字符串两端空白。split():分割字符串,返回一个分割后的字符串数组。getBytes():返回字符串的 byte 类型数组。length():返回字符串长度。toLowerCase():将字符串转成小写字母。toUpperCase():将字符串转成大写字符。substring():截取字符串。equals():字符串比较。在使用 HashMap 的时候,用 String 做 key 有什么好处?​HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的1、可变性String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。2、线程安全性String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。3、性能每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。对于三者使用的总结如果要操作少量的数据用 = String单线程操作字符串缓冲区 下操作大量数据 = StringBuilder多线程操作字符串缓冲区 下操作大量数据 = StringBufferString,是否可以继承,“+”怎样实现,与 StringBuffer 区别答:在Java中,使用“+”来串联字符串的时候,实际上底层是通过StirngBuilder实例的append()实现的。String有重写Object的 hashcode和toString吗?如果重写Equals不重写hashcode会出现什么问题?​答:String重写了Object类的hashcode和toString方法。当equals方法被重写时,通常有必要重写hashcode方法,以维护hashCode方法的常规协定,该协定声明相等的俩个对象必须有相同的hashcode.Object1.euqal(object2)为true,Object1.hashCode()==Object2.hashCode()则为true。Object1.hashCode()==Object2.hashCode()为false,Object1.euqal(object2)必定为false。Object1.hashCode()==Object2.hashCode()为true,Object1.euqal(object2)不一定为true。1.7.2 Date相关1.7.3 包装类相关自动装箱与拆箱装箱:将基本类型用它们对应的引用类型包装起来;拆箱:将包装类型转换为基本数据类型;int 和 Integer 有什么区别Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。Java 为每个原始类型提供了包装类型:原始类型: boolean,char,byte,short,int,long,float,double包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Doubleint与Integer的基本使用对比• Integer是int的包装类;int是基本数据类型;• Integer变量必须实例化后才能使用;int变量不需要;• Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值 ;• Integer的默认值是null;int的默认值是0。int与Integer的深入对比(1)由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。 Integer i = new Integer(100); Integer j = new Integer(100); System.out.print(i == j); //false(2)Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)Integer i = new Integer(100); int j = 100; System.out.print(i == j); //true(3)非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。因为非new生成的Integer变量指向的是静态常量池中cache数组中存储的指向了堆中的Integer对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的对象引用(地址)不同。Integer i = new Integer(100); Integer j = 100; System.out.print(i == j); //false(4)对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为falseInteger i = 100; Integer j = 100; System.out.print(i == j); //true Integer i = 128; Integer j = 128; System.out.print(i == j); //false对于第4条的原因: java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100)。而java API中对Integer类型的valueOf的定义如下,对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127这个Integer对象进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了。public static Integer valueOf(int i){ assert IntegerCache.high >= 127; if (i >= IntegerCache.low && i <= IntegerCache.high){ return IntegerCache.cache[i + (-IntegerCache.low)]; } return new Integer(i); } 1.8 常用的工具类库1.9 栈、堆、方法区存储的内容堆区:1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身 。栈区:1.每个线程包含一个栈区,栈中只保存基础数据类型的值和对象以及基础数据的引用2.每个栈中的数据(基础数据类型和对象引用)都是私有的,其他栈不能访问。3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。方法区:1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。1.10 日志
文章
存储  ·  缓存  ·  Oracle  ·  安全  ·  Java  ·  关系型数据库  ·  编译器  ·  API  ·  C++  ·  Spring
2023-02-02
不懂JavaScript 内存管理的前端不是好程序员!
一位客户报告说,当点击一个按钮后,Chrome关闭了该页面,该页面就失去了响应。我最初认为这可能是Chrome扩展的问题。所以,我在隐身模式下进行了测试,但问题也在隐身模式下重现。在解决这个问题之后,我花了很多时间阅读和理解JavaScript及其编译器。因此就有了该篇文章内存泄漏内存泄漏是因为不需要的内存,但由于某种原因,它没有被回收返回到内存池。JavaScript的设计方式是,一旦变量被使用,它将自动删除分配的内存,这个过程称为垃圾收集。JavaScript编译器使用两种不同的垃圾收集器,一种是主Major GC,另一种是Minor GC。更多: https://v8.dev/blog/trash-talkMajor GC: Major GC从整个堆中收集垃圾。Minor GC: 小GC收集年轻代的垃圾。Major GC:主GC是主垃圾收集器,它识别活对象和死对象,并删除死对象。但是Major GC是在主线程上运行的,所以如果经常调用GC,页面就会变得无响应。垃圾回收为什么冻结页面?Garage collector运行在主线路上,所以它会阻塞所有用户输入,因此页面变得无响应。好吧,但为什么主线程阻塞所有用户输入?简单的回答是事件循环。更详细的答案,你需要看这个视频。对事件循环和JavaScript编译器有更详细的解释。更多:https://www.youtube.com/watch?v=8aGhZQkoFbQ改善应用程序内存管理的通用建议1. 全局变量垃圾收集器从不收集全局变量的内存,因此在开始声明全局变量之前,请三思。但有时,我们会不小心引入了一个全局变量。请看代码function foo(arg) { bar = "this is a hidden global variable"; }您可能会注意到,该变量没有定义但赋值,因此它将成为一个全局变量。这里,我们泄漏了一个字符串内存,你可能认为这不会产生大问题,但它会覆盖其他一些全局方法,这会引起新的隐患。2. 引用没法回收当涉及到对象和数据绑定时,一定要关注内存。function run(){ var domObjects = $(".myClass"); domObjects.click(function(){ domObjects.addClass(".myOtherClass"); }); }一旦函数被调用,JavaScript将删除分配的内存。但是在上面的代码中,GC没有办法收集“domObjects”的内存,因为它绑定到了事件监听器。如果你想删除这些引用,你可以手动删除它们。function run(){ var domObjects = $(".myClass"); domObjects.click(function(){ if(domObjects){ domObjects.addClass(".myOtherClass"); domObjects = null; } }); }3. 字符串连接这听起来很奇怪,但是字符串连接需要额外的内存。因此,避免字符串连接,而使用模板字面量代替。4. 避免创建新对象新的内存被分配给新的对象、数组等。想办法减少这种情况。让我们看一个jQuery的例子,当你从不使用jQuery时,它会增加堆中的内存,所以我们可以避免使用jQuery链接方法。6. JSON Parse当您的应用程序在页面加载期间基于JSON进行渲染时,请考虑使用JSON.stringify和JSON.parse来处理这些数据。更多: https://v8.dev/blog/cost-of-javascript-2019#json7. 避免 try-catchTry-catch比if分配更多的内存。但是try-catch对于大多数应用程序都是必要的,如果你分析得当,你可以把它们变成try-catch。请看看下面链接中的问题,由node js环境中的try-catch引起的内存泄漏问题。您将获得一些关于try-catch和内存泄漏的知识https://github.com/nodejs/node/issues/350488. 创建新窗口当创建一个新窗口时,请把废弃的window引用设置为null。当关闭窗口时,只调用Minor GC,因此只清除年轻代内存块。如果您想检查JSHeap内存占用情况,请在您的控制台上输入此代码:memory.usedJSHeapSize9. Callbacks回调如果事件的回调频率非常高,考虑避免它。让我们以滚动为例。当您将滚动事件绑定到窗口用户向下/向上滚动时,回调调用的频率非常高。可以使用 Underscore.js 中 _.throttle 和 _.debounce 来减少回调执行,避免页面卡顿。function log( event ) { console.log( $(window).scrollTop(), event.timeStamp ); }; // 控制台记录窗口滚动事件,触发频率比你想象的要快 $(window).scroll( log ); // 控制台记录窗口滚动事件,每250ms最多触发一次 $(window).scroll( _.throttle( log, 250 ) ); function ajax_lookup( event ) { // 对输入的内容$(this).val()执行 Ajax 查询 }; // 字符输入的频率比你预想的要快,Ajax 请求来不及回复。 $('input:text').keyup( ajax_lookup ); // 当用户停顿250毫秒以后才开始查找 $('input:text').keyup( _.debounce( ajax_lookup, 250 ) );更多请阅读https://blog.coding.net/blog/the-difference-between-throttle-and-debounce-in-underscorejs
文章
Web App开发  ·  JSON  ·  JavaScript  ·  前端开发  ·  Java  ·  编译器  ·  程序员  ·  数据格式
2023-02-01
Java基础--数组的认识(通透!!!)
一/简单回顾基本知识点数据类型常量变量运算符号分支结构:if switch循环结构: for while do...while​ 循环嵌套关系: break continue 标记二/ 引子之前我们在学循环的时候,讲到一个例子,不知道大家还记不记得。int score=90; 根据score成绩来进行区间的判定,不及格/及格/中等/良好/优秀/满分。之前解决这个问题的时候,我们用到if else语句,后来又可以用switch语句来解决。那么现在,我们重新来想一下这个问题。①score是一个变量空间(栈内存中的一块空间),可以理解为一个小容器。小容器里面存储的是一个学生的成绩。②变量有什么特点?1.变量在创建的时候,类型是固定的。2.空间内的内容只能存放一份。现在再来想一个问题。如果想将5个同学的成绩都存起来,那我们就需要5个变量。每次多一个变量,就会多一个名字,不方便。在现实中,我们就会给他们编一个班级,以班级为的单位。这样就方便多了。那么,我们同样可以用这种方式来存储数据。下面要讲到,这种方式就是用数组来存储。三/ 数组(1)概念来看看数组这俩字:数:数据类型组:一组元素数组是一组数据类型相同的数据的结合。数组也可以看作一份容器。和变量不同的是,变量空间只能存放一份,而数组可以存储一组内容。将这些数据统一在一起管理起来,更加方便。(2)性质之前说过的基本数据类型有byte/short/int/long/float/double/char/boolean。而引用数据类型有数组/类/接口/枚举/注解等,包括之前咱们接触过的String/Scanner/Math。我们可以看到,数组是一个引用数据类型。(3)写法1)数组的声明(创建)①起名字在学变量的时候,咱们是先定义一下,如int score。那么,同样,在用数组之前,也需要定义一下。刚才说过,数组和变量类似,可以当作一个容器,存在于栈内存或堆内存当中,在里面开辟了一个空间。既然是开辟了一个空间,那么就要先给它起一个名字。②添加数据类型那么数组里面存什么呢?通过上面的概念,我们知道,数组是一组数据类型相同的数据的结合。虽然数组里面是一组数据,但是要求,数据类型要相同!!!所以我们在给数组声明的时候,要说明这里面存的是什么。在名字前面,我们需要添加数据类型。③添加中括号现在我们得到这样的效果:数据类型 数组名字;咦,这样不就和变量声明格式一样了吗?怎么表示它是一个数组呢?为了区分,我们在数据类型之后,添加一个中括号[ ],来表示它是一个数组形式。如果没有中括号,就表示是一个变量空间,加了中括号,就是数组。2)举例通过上面的分析,我们可以得到数组的定义:数据类型[] 数组名字;。那么我们现在举几个例子。我想创建一个数组,来存储一些int类型的整数,可以这样写:int[] x;。又比如,存储一组字符类型的数据,可以这样写:char[] y;。存储一些布尔类型的值,可以这样写:boolean[] z;。存储一些字符串形式的数据,可以这样写:String[] m;。:star: ==数组本身是一个引用数据类型,但是,数组内存储的类型,可以是基本类型,也可以是引用类型。==(里面的每一个元素,什么都可以,只要是相同的类型就行)3)补充在书写数组的时候,我们会加上一组中括号表示它是数组。那这个中括号的位置可以写在哪里呢?在上面,我们将它写在了数据类型的后面,就是这样:int[] x;。但是,写在数组名字后面也是可以的,就像这样:int x[];。在这里,推荐大家写第一种,写在数据类型后边的,比较标准。int[] x;这种写法,可以理解为,x是变量名,前面的int[]整体算作一个类型。在这里,主要想告诉大家,两种写法都可以,都好用,编译器都可以识别,但是规范来说,还是第一种。在笔试题中,以下这几种写法都是可行的啦。int[] x; -->规范 int []x; -->别扭的很(和上面的写法类似,只不过将中括号后移了一点) int x[]; -->也可以这样写,但是识别的时候,不好看。(4)数组的初始化数组声明之后,我们需要往里面存东西。数组是一个引用数据类型,既然是引用数据类型,赋值有一个专业的名词,叫做初始化。数组的初始化:创建数组并且给数组赋值。数组的初始化有两种:静态初始化/动态初始化。1)静态初始化举个例子,现在我们要创建一个数组,里面存储整数类型,起个名字叫array。那么可以这样定义:int[] array;现在要将它初始化,直接将值赋给它?有的小伙伴想这样写,都不对哈,看下面的情况(都不对的呦):<1> 给一个常量是不对的 int[] array=10; -->直接给一个10,肯定不对,数组里面又不是只有一个10,是有好多个这样的类型。 <2> 给一个字符也不对 int[] array=`a`; <3> 给一个字符串也不对 int[] array="abc"; -->类型不统一:star:==数组是一个引用数据类型,我们以后,见到的所有引用数据类型,只要想创建,通过的方式是统一的。要new一下。==这里,就是开辟一个新的数组,给array赋上。那new什么呢?前面都规定好了,是一个数组类型(int[])。那么我们肯定要new一个和前面类型一致的!前面的类型是什么,后面就要new什么。现在我们要的是一个int[]数组类型,那么后面就要new一个数组类型啦。就像这样:int[] array=new int[];还记得之前我们说过的Scanner吗?这样写的来着:Scanner x=new Scanner();,这里是圆括号,数组是方括号。这两个写法很像,x和array都是变量名,Scanner和int[]都是一个类型,后面new的过程,都相当于开辟了一个新的空间。其实这里我们称之为对象更合理一点。现在我们new过了,那数组里面有什么呢?我们就需要在后面加上一组大括号,就像这样:int[] array=new int[]{};。大括号表示一组元素,每一个元素都是一个int类型的整数。现在我们只需要在大括号里面添加数值即可,比如添加10,20,30,40,50这些数据。那么就可以这样写:int[] array=new int[]{10,20,30,40,50};。这样就写好了,数组的静态初始化的标准写法。从这个写法上,我们可以看到,数组的长度是5(数组里面的数据的个数);还可以看到每一个元素。静态初始化,可以看到数组的长度和内容。好啦,标准的静态初始化写法,我们都已经知道了。现在再来看一下,它的简洁写法:int[] array={10,20,30,40,50};。可以将new的过程省去不写。为啥可以这样呢?数组变量名(array)之前,已经有了定义类型( int[] )。那么,后面new出来的类型肯定和前面定义的一致。==在数组定义的时候,前面如果有定义,那么后面的new可以省去不写,变成简洁的写法。==但是,如果现在只是一个变量名,就不能省去new的过程了!!!就像这样:int[] array; ... ... ... ... array=new int[]{10,20,30,40,50};1.前面如果有数组类型的定义,就可以省去后面new的过程;2.但是,如果前面只是一个变量,虽然之前这个变量array已经定义好了类型,但是此时编译的时候,它分不清变量到底什么类型的。这个时候,就必须new一下。最后,总结一下,静态初始化的两种写法:int[] array=new int[]{10,20,30,40,50}; -->标准写法 int[] array={10,20,30,40,50}; -->简洁写法2)动态初始化:walking: 提示:先跳过这个往后看,最后再来看这个,要不然可能看不懂。跳过看不影响后边的啦,不用担心。大致和静态初始化一样,但是在创建过程,只给数组的长度,没有元素。比如,我们有5个元素,可以这样写:int[] array=new int[5];。注意几点:①创建0长度是可以的。只不过是创建一个0长度的数组而已。可以来试一下:public class TestArray{ public static void main(String[] args){ int[] array=new int[0];//创建了一个长度为0的数组 } }看一下执行结果:我们可以看到,编译(javac)和执行(java)都是可以的。(这里没有做输出,就没有输出什么啦)②负数也是可以的,但只能通过编译(语法结构正确),不能执行结果。再来试一试:public class TestArray{ public static void main(String[] args){ int[] array=new int[-1];//创建了一个长度为-1的数组 } }看一下错误,是运行时异常:报错:Negative Array Size Exception,数组的长度不合法(创建数组的时候长度给了负数)。总结:它认为0是一个正常的整数,可以用。​ 但是,0长度的数组,创建出来,什么东西都塞不进去,创建出来没有意义。③动态初始化,只有长度,没有元素,是真的没有元素吗?我们来试一下:public class TestArray{ public static void main(String[] args){ int[] array=new int[5]; System.out.println(array[0]); //访问数组的第一个元素,如果数组里面没有元素,第一个元素也就不存在 } }看一下运行结果:运行结果是0。大家可以自行尝试其他的位置,可以发现,出来的结果都是0。我们来全部访问一遍,加强for循环来试一试。(加强for循环后边有讲,不懂的小伙伴转移后方)public class TestArray{ public static void main(String[] args){ int[] array=new int[5]; for(int value:array){ System.out.println(value); } } }运行结果:可以看到,最终结果都是0。由此可见,这里说的,动态初始化,有长度,没有元素,不是真的没有元素,默认值是0。为啥是0呢?1.因为是整数呀,整数默认值就是0。2.如果是浮点数,默认值就是0.0。3.如果是字符型,默认值就是0对应的char值(97-->a ,65-->A,48-->'0')。这个大家可以去试一下,打印不出来的,输出的值看不见。emm,给大家示范一下吧。代码如下:public class TestArray{ public static void main(String[] args){ char[] array=new char[5]; for(char value:array){ System.out.println(value); } } }来看一下执行结果:可以看到,打印出来了5个数,但是没有显示。4.如果是布尔型,默认值就是false。5.如果是引用数据类型,默认值就是null。(比如数组,虽然数组是引用数据类型,但是数组里面存储的可以是基本数据类型,也可以是引用数据类型)如果数组里面每个元素是基本数据类型,那么默认值就是上面的其他情况。如果数组里面存储的是引用数据类型。比如创建一个数组,存储String类型的数(引用数据类型)。数组里面每个元素都是引用数据类型,那么默认值就是null。咱们可以试一试:public class TestArray{ public static void main(String[] args){ String[] array=new String[5]; for(String value:array){ System.out.println(value); } } }看一下运行结果:④动态初始化之后,都是默认值,并不是我想要的呀。我们往里面存了东西,这个数组才有效。还是拿最开始的整数类型举例。访问元素,然后存值即可(看不懂如何访问的小伙伴,看后边的讲解哈)。代码:public class TestArray{ public static void main(String[] args){ int[] array=new int[5]; array[0]=10; array[1]=20; array[2]=30; array[3]=40; array[4]=50; //可以有array[5]吗?不可以的,会出现Array Index Out Of Bounds Exception的错误(后边有讲呦) } }最后,总结一下,动态初始化的写法:int[] array=new int[5];3)总结静态和动态都有new的过程。静态初始化:有长度,有元素。动态初始化:有长度,没有元素(不是真的没有,是默认值),需要往里面放东西才能用。==所有引用类型,都得用new关键字来创建。==除非有特殊情况,比如Math.random();,虽然是引用类型,但从不用new对象。为啥呢?因为random()方法是一个静态方法,是一个static修饰的静态方法。这个静态方法,在整个类(Math类)中就一份,不需要创建对象,通过类名可以直接访问!!!(5)数组元素的访问并应用1)访问数组元素数组的定义和初始化,咱们都已经学会了。那么,数组作为一个容器,里面存的值得用呀。那么我们如何访问数组里面的元素呢?举个小例子:现在我们定义了如下数组,想访问10这个元素,该怎么办呢?public class TestArray{ public static void main(String[] args){ int[] array=new int[]{10,20,30,40,50}; } }以前定义变量的时候,想要拿出来用,只需要把变量名拿过来即可。但是现在,是一个数组,数组名array代表的不是一个数,而是5个数。我们这样来理解:array相当于一个班级,里面有5个人,叫10,20,30,40,50。现在只有一个班级名array,而没有5个人的人名,现在想要10,咋办?我们可以这样,array班级中的第一个同学。通过班级名找到第一个同学,就找到10啦!那么,同样,我们可以访问第二个,第三个同学。:star:==可以通过访问元素在数组中的位置,来访问每个元素。==数组中元素的位置如何定义?利用索引(index),即数组中每个元素所在的位置。计算机中,是从0开始数的。索引号是从0开始数的。我们要访问元素,就可以这样访问:array[index];。现在我们要访问数组第一个位置,就可以这样写:array[0];。2)取出数组元素既然拿出来了,就要存着用啊,可以用一个变量将它存起来,这个变量的数据类型必须要和这个拿出来的数据的数据类型一致。比如这里,就要用int类型的变量来存储拿出来的数据。这里定义一个变量value来存储这个数。可以这样写:int value=array[0];。我们可以输出一下这个value,来看看输出的值是不是数组第一个数。代码如下:public class TestArray{ public static void main(String[] args){ //数组array的初始化 int[] array=new int[]{10,20,30,40,50}; //从数组内取得某一个位置的元素,并存入变量value中 int value=array[0]; //输出 System.out.println(value); } }运行一下(这里用dos窗口运行的,具体怎么操作可以看我之前的文章):可以看到,结果输出就是10,就是数组第一个数。3)替换数组元素现在我们想让40变成60。①先访问40这个元素,它是在数组里面的第4个数,索引号是3。(索引是从0开始计数的呦)这样写array[3]。②将400存入这个位置即可,这样写array[3]=400;。就当这个位置是个变量,将400赋值给这个变量即可。③输出这个位置的元素,看一看是不是真的换啦。代码如下:public class TestArray{ public static void main(String[] args){ int[] array=new int[]{10,20,30,40,50}; //向数组内的某一个位置存入元素 array[3]=400; System.out.println(array[3]); } }输出结果:可以看到,数组第4个元素,变成了400。数组元素的每一个位置,可以认为是一个小变量,一个小格子。可以从这个小格子里面取值,也可以往里面放值。4)数组元素的遍历(轮询)数组元素的遍历即取出数组所有元素。刚才我们取的都是一个元素,现在我想取出数组中所有的元素。<1> 分析我们访问每个元素,是这样写的:array[index]。这时候,我们要想把元素都拿出来看一看,就需要用到循环。【第一次是array[0],第二次是array[1]……(每次都是做同样的事情,而且索引号有规律)】用什么循环呢?for和while都一样。我们这里用for循环演示。(为啥想到循环,是因为我们想让程序每次都给咱们做同样的事情:取数组的值):question: 这里需要循环几次呢?5次(一共5个数嘛)。5次循环,从几数到几,比较方便呢?循环的目的只是执行5次,我想从几开始数都可以,只要是5次就好啦。比如,100-104,或者1000-1004,都可以。循环跟取值从几开始没有关系的,但是5次从几次开始数比较方便?如果循环乱开始数,里边每次取数组元素,就不太方便。因为数组本身还有索引号,还得重新找个变量来控制它。如果这五次刚好和索引号对上,那么就可以将循环的变量来当索引号!!!<2> 正常循环遍历:red_car: 根据上面的分析,循环的开始值,可以从0开始(索引号index从0开始的)即index=0。用它既当循环的变量,又可以当索引的变化。这样比较方便,省了一个变量啦。那么,到什么时候结束?index<=4或者index<5。这里的index就有两个作用。第一,控制循环次数变化(5次);第二,控制索引变化(index变化和索引变化对上了)。综上,循环部分就可以这样写for(int index=0;index<5;index++){}。代码如下:public class TestArray{ public static void main(String[] args){ int[] array=new int[]{10,20,30,40,50}; for(int index=0;index<5;index++){ //每一次做一样的事情,即取数组的值,一共5次 int value=array[index]; //将取得的值放入变量value中 System.out.println(value); //将它们输出看一看 } } }编译运行结果:<3> 增强for循环上面的for循环,是一般写法,还有一种增强的写法。不过对JDK版本有要求。JDK 1.5版本之后,有了很多新的特性,其中有一个就是:增强for循环(ForEach)。刚才我们写的for循环有3个要素,用两个分号表示,三个要素必须执行,即:for(;;){ }加强for循环之后呢,小括号之间不再是三要素了,中间也不是分号了。中间写的是冒号。冒号将括号隔成两个部分,分别是,自己定义的变量(作用是接收数组内每一个元素):遍历的数组array,即:for(自己定义的变量(作用是接收数组内每一个元素):遍历的数组array){ }那么上面的问题,就可以这样来写啦:public class TestArray{ public static void main(String[] args){ int[] array=new int[]{10,20,30,40,50}; for(int value:array){ //冒号后面是循环的数组,即array //冒号前面是人为定义的变量,用来接收数组内每一个元素 //(array数组内每一个元素都是int类型,那么用来接收的变量也应该是int类型的) System.out.println(value); } } }然后编译运行,看一下结果:<4> 总结正常的for循环和加强的for循环,都要会!!!存在即合理,两个for循环都有各自的好处。①正常的for循环优点:​ 有三个必要条件,可以通过index索引找到某一个元素的位置;​ 而且可以往数组里面存值或取值,因为是直接访问数组的元素位置。缺点:​ 写法相对比较麻烦而已。②增强for循环优点:​ 有两个条件,一个是用来取值的变量,一个是用来遍历的数组。​ 没有index索引,写法相对比较容易。缺点:​ 没有index索引,找不到元素到底是哪一个。​ 而且只能取值不能存值。(如果取值,直接拿出来用就好了;但如果存值,可不行)​ 有的小伙伴可能会这样写,让value直接等于100。​ 这样写是不对的,100存到value里面了,而不是数组里面!for(int value:array){ value=100; //这样写是不对的,100是存到value里面的,而不是数组里面! System.out.println(value); }做正常遍历输出的时候(只想看看结果),加强for比较容易。想要操作数组中每个元素,甚至是某一个元素,索引号很重要,正常的for循环就很好。(6)索引的范围问题我们之前提到了索引,这个索引,是有范围限制的。比如上面的数组,我们可以访问第6个元素吗?(索引是5)来试一试。代码如下:public class TestArray{ public static void main(String[] args){ int[] array=new int[]{10,20,30,40,50}; //目前这个数组有5个元素,索引为0~4 int value=array[5]; System.out.println(value); } }运行结果:明显可以看到,是可以编译的,但不能运行!这里是运行时异常。我们说一个错误,得说明是编译不行,还是运行有问题。想要知道最终结果,是有两步操作的。第一步翻译(javac),翻译了不一定要执行呀。翻译是,写完代码,遵循了正确的语法结构。翻译过去了,就是遵循了合适的语法结构。但是遵循了语法结构,就一定可以用吗?不见得啊。那就要看第二步运行(java)了。上面的错误,就是明显的翻译正确,但不能用。当然不能输出啦。翻译的时候,5是一个有效数字,当然可以编译。但是执行的时候,在数组的第5个位置上没有这个元素,所以不能输出。这里出现的错误,就是运行时异常(javac可以过去,一旦java运行就不行了)。还有一种是编译时异常(javac就过不去),即语法错误。在未来开发过程中,找异常也是一个很重要的环节,不能小视。光写不会调,那是百搭。在这里,说一下几个异常,大家见一个记一个就好(记英文)。不仅要记异常的英文,还要知道异常点在哪里(即因为什么引起的异常),以后遇到这种异常,要学会自己调试过来。<1>之前说过一个输入,Scanner input.nextInt();。这个输入,只能输入一个数字。如果输入的不是数字,就会出现这样的异常Input MisMatch Exception,输入类型不匹配。<2>这次的异常叫Array Index Out Of Bounds Exception,数组索引超出边界异常(数组索引越界)。这个异常如何产生的?就是数组索引越界了。数组的index索引是:开始(0)~结束(数组长度-1)。本次案例中,索引是0~4,出现在这个范围之外的都是越界。<3>上面讲动态初始化的时候,说过一个不常见的错误,在这里一起说了吧。Negative Array Size Exception,数组的长度不合法(创建数组的时候长度给了负数)。(7)总结数组元素的访问:通过元素在数组中的位置来访问。可以存值或取值。位置--->index索引索引是有取值范围的,范围是从【(0)开始~(数组长度-1)结束】,是闭区间,两边都能取到。如果数组的索引超出了上述范围,会出现一个运行时异常:Array Index Out Of Bounds Exception。
文章
存储  ·  Java  ·  编译器  ·  索引  ·  容器
2023-02-01
Java基础--手把手教你如何从键盘录入信息
从键盘录入信息一、前奏1、创建扫描仪对象Scanner有扫描仪的意思,sc是自己取的名字(有的人喜欢用input),new Scanner是创建一个Scanner对象,System.in代表电脑的键盘。 /*创建扫描仪对象*/ Scanner sc=new Scanner(System.in);即,扫描电脑的键盘。2、导入包将第一步写完,我们在编译器里面看到,Scanner标红了,报错了!:question: 为啥报错呢?我们要使用Scanner,需要先找到它的路径!我们需要导入一个包。注意是在最上方导入包。/* 导入Scanner 导包 */ import java.util.Scanner;import是导入的意思。后面一串是文件存在的路径,这里指的是Scanner存在的路径。:question: 那这个路径在哪儿呢?我们可以在左侧找到。"External Libraries"-->"<1.8(2)>"-->"rt.jar"-->"java"-->"util"-->"Scanner"。具体演示:(1)点击打开“External Libraries”。(2)点击打开“<1.8(2)>”。(3)找到“rt.jar”。(4)点开“java”。(5)找到“util”。(6)往下找,可以看到“Scanner”。:question:那我不知路径咋办?我不知道Scanner在这儿啊?(1)第一种方法别急,我们先回到第一步,写完报错的时候。将鼠标挪到红色Scanner上,会出现一个提示。点击Import class。然后选择第一个Scanner(java.util)。就会自动导入路径啦。(2)第二种方法还有一种方法,将光标放在Scanner上,然后直接按快捷键Alt+Enter。然后再按回车(Enter)。然后再按回车即可自动引入啦。二、从键盘录入信息1、输入整数先从键盘接收一个整数--->sc.nextInt(),然后创建一个整型变量b,将从键盘接收的整数保留起来--->int b。具体写法如下: /*从键盘接收一个整数*/ int b=sc.nextInt();然后我们输出变量b。System.out.println("您输入的是:"+b); //前面输入的是字符串,后面是一个变量,中间的"加号+"是在做文字的拼接接下来我们来运行这个代码。右键run之后,我们会发现,控制台什么也没有输出,但是小红灯还是亮着的。亮着说明这个程序还在跑。为啥没有东西输出?当然没有输出啦,现在正在执行这一行,等待我们输入数据呢。那我们现在输入一个整数。然后按下回车,执行下一行代码,输出相应的内容。可以看到,红灯没有了,程序也结束啦。现在写的这个代码,还是很影响观感的,没有提示语句,控制台空空白白的,咱也不知道什么时候输入啊。我们可以在输入之前,加上一行代码,用于提示用户,该从键盘输入内容啦!这样写就可以了: /*提示用户*/ System.out.println("请录入一个整数");注意是写在这个位置哦,要在从键盘输入之前提示才妥当!这样的话,输出的时候,就比较直观了。我们运行代码:这个提示语句,是不是很直观呢?现在咱们输入10,然后回车。2、输入浮点数先从键盘接收一个浮点数--->sc.nextDouble(),然后创建一个变量c,将从键盘接收的浮点数保留起来--->double c。具体写法如下:double c=sc.nextDouble();然后和上面输入整数一样,咱们需要在键盘输入之前提醒一下用户,输入一个小数。System.out.println("请录入一个小数");最后输出即可。System.out.println("您输入的是:"+c);整体就是这样的:然后运行。我们从键盘敲一个小数,然后回车,可以看到最终结果。3、输入字符串先从键盘接收字符串--->sc.next(),然后创建一个字符串变量s,将从键盘接收的字符串保留起来--->String s。具体写法如下:String s=sc.next(); //next后面什么都不放的意思就是,输入的是纯文字。然后和上面输入整数一样,咱们需要在键盘输入之前提醒一下用户,输入字符串。System.out.println("请输入其他内容");最后输出即可。System.out.println("您输入的是:"+s);整体就是这样的:然后运行。我们从键盘敲一行字符串,然后回车,可以看到最终结果。三、释放资源当结束之后,我们需要释放一下资源。在结尾写上这一行代码: /*释放资源*/ sc.close();即关闭扫描仪。注:不写这行代码,程序也是可以运行的。但是,关了更好啊,用完了就应该把还回去是吧。四、补充说明1、输入与接收的数据类型不匹配(1)我们第一个要求输入的是一个整数,但是如果输入了一个小数,会怎么样呢?输入,按回车之后,会发现报错。出现了一个InputMismatchException的错误。Input是“输入”的意思,Mismatch是“不匹配”的意思,Exception是“异常”的意思。就是输入的数据,和要求输入的数据类型不匹配!我们在控制台,可以看到报错的具体位置(问题在哪儿产生的)。这里也顺便教大家如何查看报错的位置啦,学到就是赚到呀~具体含义:在scannerDemo这个包,Test2里面,主方法main里的第15行。总体来说,不是代码写错了,是输入的数据和接收的数据类型不匹配!(2)我们第二个要求输入的是一个小数,但是如果输入了一个整数,会怎么样呢?输入,按回车之后,会发现并没有报错。但是输出值还是有小数点位的。你输入整数之后,它会带一个.0处理,然后当成一个浮点数存起来。我们知道,整数的数据是可以给浮点数数据类型的变量赋值的。2、输入带有空格的字符串我们输入字符串如果带有空格,会输出什么样的结果呢?输入完成,按回车,会发现能录入的仅仅只有空格前面的内容!这也算,next接收文字的一个弊端吧。:question:那么如何让空格之后的内容也被接收呢?我们将next()改为nextLine()。【LIne有行的意思,就是一整行的意思】String s=sc.nextLine(); //sc.next();我们再次尝试。好家伙!程序直接结束了?!我还没有录入呢,就直接结束了?!为哈会这样呢?nextLine有个毛病,它会受到上面输出的影响,好像录入进来了,但其实啥也没有录进来。如果想要用nextLine的话,前面就不要输出任何信息!!!不要有任何输出语句。nextLine一般不推荐使用,一般使用next,只不过会有空格的问题而已。:question: 如果就想用nextLine那咋办?那我们就要在任何一个输出语句之前,先用nextLine就好啦。如图(注意在红框框住的那一行之前不要有任何的输出语句):我们再次执行。此时正在等待我们录入。我们输入带有空格的字符串,然后回车。我们可以看到,完整的输出啦!nextLine就是,有空格没有关系,直接把一整行都录入进来啦。但如果前面有输出语句,会严重影响它的功能!!!所以不怎么用它。五、全部代码在此将全部代码放在这儿,供大家学习使用。package scannerDemo; /* 导入Scanner 导包 */ import java.util.Scanner; public class Test2 { public static void main(String[] args) { /*创建扫描仪对象*/ Scanner sc = new Scanner(System.in); String s1 = sc.nextLine(); System.out.println(s1); /*从键盘录入信息*/ /*提示用户*/ System.out.println("请录入一个整数"); /*从键盘接收一个整数*/ int b=sc.nextInt(); System.out.println("您输入的是:"+b); //前面输入的是字符串,后面是一个变量,中间的"加号+"是在做文字的拼接 /*从键盘接收一个浮点数*/ System.out.println("请录入一个小数"); double c=sc.nextDouble(); System.out.println("您输入的是:"+c); /*从键盘接收文字*/ System.out.println("请输入其他内容"); String s=sc.nextLine(); //sc.next(); //next后面什么都不放的意思就是,输入的是纯文字。 System.out.println("您输入的是:"+s); /*释放资源*/ sc.close(); } }
文章
Java  ·  编译器
2023-02-01
字符串常量
1.内存分布一个编译的C程序占用的内存分为以下几个部分:1、栈区(stack)—也称自动类型存储区,由编译器自动分配释放,存放函数的参数值,局部变量的值等,例如函数调用结束后自动释放。2、堆区(heap)—也称动态分配内存区,由程序员分配释放,从分配到程序结束为止,若不释放,程序结束时可能由OS回收,比如malloc分配的内存,free释放的内存。3、全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放。4、文字常量区—常量字符串放在这里,程序结束后由系统释放。5、程序代码区—编译后的程序代码放在这里。来看一个具体的C程序下图所示为可执行代码存储时结构和运行时结构的对照图。一个正在运行着的C编译程序占用的内存分为代码区、初始化数据区、未初始化数据区、堆区和栈区5个部分。(1) 代码区存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。总结:你所写的所有代码都会放入到代码区中,代码区的特点是共享和只读。(2) 全局区全局区中主要存放的数据有:全局变量、静态变量、常量(如字符串常量)全局区的叫法有很多:全局区、静态区、数据区、全局静态区、静态全局区这部分可以细分为data区和bss区2.1 data区data区里主要存放的是已经初始化的全局变量、静态变量和常量2.2 bss区bss区主要存放的是未初始化的全局变量、静态变量,这些未初始化的数据在程序执行前会自动被系统初始化为0或者NULL2.3 常量区常量区是全局区中划分的一个小区域,里面存放的是常量,如const修饰的全局变量、字符串常量等在VS下运行结果如下:总结:全局区存放的是全局变量、静态变量和常量在程序运行后由产生了两个区域,栈区和堆区。(3) 栈区(stack)栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。vs运行效果如下(4) 堆区(heap)堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。vs运行效果如下:当我们把几个案例放在一起执行,就可以看到内存将每个区域划分的很有条理。每个区域互不干涉,区域中的数据地址也是非常接近的2.字符串常量的存储首先,毫无疑问,即使是常量(字符串常量)也是要占据空间的。一般来说,基本类型(整型、字符型等)常量会在编译阶段被编译成立即数,占的是代码段的内存。(代码段是只读的,而且不允程序员获取代码段的地址,所以在c++中,尽量不为const分配数据段的内存,但是一旦取cosnt的地址,就不得不分配了,但是读const的时候,依然是从代码段读取那个立即数)。当然,占代码段的内存一般不在我们常说的“占内存”范围中。代码段不是寄存器。而字符串常量或基本类型的常量数组占用的是数据段内存。程序中但凡出现“XXX”形式,这都是代表字符串常量,是要事先存储在程序的只读数据区的。void test(){   char name[32] = "hello,world"; // "hello,workd" 存储在程序的只读数据区, name 存储在test函数的栈区。 }字符串常量的存储:字符串常量使用一对双引号括起来的字符序列,与基本类型常量的存储相似,字符串常量在内存中的存放位置由系统自动安排。由于字符串常量是一串字符,通常被看作一个特殊的一位字符数组,与数组的存储类似,字符串常量中的所有字符在内存中连续存放。所以,系统在存储一个字符串常量时先给定一个起始地址,从该地址指定的存储单元开始,连续存放该字符串中的字符。显然,该起始地址代表了存放字符串常量首字母的存储单元的地址,被称为字符串常量的值,也就是说,字符串常量实质上是一个指向该字符串首字符的指针常量。例如字符串"Hello"的值是一个地址,从它指定的存储单元开始连续存放该字符串的6个字符('H' 'e' 'l' 'l' 'o' '\n')字符数组与字符指针如果定义一个字符指针接收字符串常量的值,该指针就指向字符串的首字符,例如:char sa[]="array "; char *sp="point "; printf("%s", sa); //数组名sa作为printf的输出参数 printf("%s", sp); //字符指针sp作为printf的输出参数 printf("%s \n", "string"); //字符串常量作为printf的输出参数输出:array point string调用printf()函数,以%s的格式输出字符串时,作为输出参数,数组名sa、指针sp和字符串"string"的值都是地址,从该地址所指定的单元开始连续输出其中的内容(字符),直到遇到'\0'为止。由此可见,输出字符串时,输出参数给出的起始位置(地址),'\0'用来控制结束。因此,字符串中其它字符的地址也能作为输出参数。例如:printf("%s", sa+2); //数组元素sa[2]的资质作为输出参数 printf("%s", sp+3); //sp+3作为起始数组 printf("%s \n", "string" +1 ); //t作为起始输出输出:字符数组与字符指针都可以处理字符串,但两者之间有重要区别,如:char sa[]="This is a string"; char* sp="This is a string";字符数组sa在内存中占用了一块连续的单元,有确定的地址,每个数组元素放字符串的一个字符,字符串就存放在数组中。字符指针sp只占用一个可以存放地址的内存单元,而不是将字符串放到字符指针变量中去。如果要改变数组sa所代表的字符串,只能改变数组元素的内容。如果要改变指针sp所代表的字符串,通常直接改变指针的值,让它指向新的字符串。因为sp是指针变量,它的值可以改变,转向指向其它单元。当定义字符指针后,如果没有赋值,指针的值是不确定的,引用未赋值的指针可能出现难以预料的结果:char* s; scanf("%s",s);没有对指针s赋值,却对s指向的单元赋值,如果该单元已经分配给其他变量,其值就改变了。所以以下写法str有确定的存储单元,这才是正确的:char* s,str[20]; s=str; scanf("%s",s);为了避免引用未赋值的指针造成的危害,在定义指针时,可先将它的初值置为空,如:char* s=NULL;一般在使用指针类型后,为避免出现内存泄漏,都需要手动释放内存,如:char*s =new char[128]; delete []s; s = NULL;但如果是像 const char* str 这种指针,则不需要手动释放内存。这是因为 const char* str 定义的是一个指向常量的指针。如果str是局部变量,则字符串会随着变量所在的函数的退出而自动释放;如果str是全局变量,则程序退出时才自动释放。#include<stdio.h> int main(void){ //字符串指针 const char* s = "Hello World!"; printf("%s\n", s); //字符串数组 char ch[] = "Hello World!"; printf("%s\n", ch); //整型变量和指针 int* i, a = 233333; i = &a; printf("%d\n", *i); //字符型变量和指针 char* cha, b = 'a'; cha = &b; printf("%c\n", *cha); return 0; }输出结果:在处理整型和字符型等时,printf()函数输出时指针变量前需要带上 * 号而处理字符串时,输出时指针变量前则不需要带 * 号。3.字符串常量内存释放问题#include <iostream> using namespace std; char *str1() { char *str="hello world"; return str; } char *str2() { char str[]="hello world"; //str[]在栈上,子函数结束后自动释放内存,返回的其实是首地址 return str; //拷贝后,返回和”hello world“无关,返回存在str[]中的内容 } char *str3() { static char str[]="hello world"; return str; } int main() { char *str=NULL; str=str1(); cout<<"指针指向内存内容:"<<str<<endl; //输出hello world str=str2(); cout<<"栈内容:"<<str<<endl; //输出乱码 str=str3(); cout<<"静态存储区内容:"<<str<<endl; //输出hello world return 0; }字符串常量存放在静态存储区。str1返回指针指向内存首地址,由于字符串常量存在静态区,所以内容不变,还是“hello world”str2将静态存储区的内容拷贝一份到栈中,由于栈在str2结束时释放栈内存,所以输出为乱码str3返回存在静态存储区的内容,“hello world”可以返回一个局部变量的值,也可以返回一个局部静态指针的地址,但不应该返回一个局部自动指针的地址int get() { int a=152; return a; //可以正确返回 }int *get() { int a=152; return & a; //无意义 }4.字符串常量生命周期字符串char s="hello"; 与char s[]="hello";,看似都是将hello字符串的地址赋值给指针 p。但是前面一个表达式是字符串常量的地址赋值给指针,该指针指向的字符串中的字符是不允许被更改的。而后面一个表达式是将该字符串的每一个字符赋值给数组,该指针指向的数组的首地址,而数组成员是变量,因此可以允许被更改赋值。下面讨论关于字符串常量在内存中存在生命周期的的问题。问题如下:假如char *s0="hello"; s0="world";就是说如果开始s0指针指向“hello”这个字符串这个常量的地址,但是当s0指针指向了“world”符串的时候,那么“hello”这个字符串在内存中的是否会被释放掉,还是会一直存在内存中,直到程序结束。我们不妨先试着写几行代码,通过运行结果来进行分析:案例 一:代码:#include<stdio.h> int main() { char *s0,*s1,*s2; s0="hello"; s1="hello"; s2="hello"; printf("%p\n",s0); printf("%p\n",s1); printf("%p\n",s2); return 0; }运行结果:三个指针变量的指向的地址都是相同的,如图分析:通过运行结果我们可以看出,赋给不同字符指针的相同的字符串,所有的指针都指向了相同的地址。但是案例一由于三个指针变量都在同一个函数,因此不能看出其生命周期,但是可以为下面的案例解释做好铺垫。案例二:代码:#include<stdio.h> char *s0="hello"; void a(){ char *s1="hello"; printf("%p\n",s1); s1="world"; } void b(){ char *s2="hello"; printf("%p\n",s2); s2="world"; } int main(){ char *s3=(char *)0; printf("%p\n",s0); a(); b(); s3="hello"; printf("%p\n",s3); return 0; }运行结果:四个字符指针变量指向的地址还是一样的。分析:这就说明问题了,在函数a(),b()中,局部指针变量s1,s2都指向”hello“字符串,但是在退出之前有改变指针指向其他字符串,但是两个字符串指针打印出来的地址还是一样的,说明在使用了字符串常量后,该字符串常量并没有在随着函数的结束而消失,而是依旧存在于内存中,因此当其他函数中使用一样的字符串常量时,指向的依旧是跟还是一样的地址。但是也许有人会问到,因为有在全局字符指针变量 char *s0 ="hello";指向了该字符串常量,会像全局变量一样,在真个程序运行都不会释放,因此其他函数调使用该字符串常量时才指向了同一地址。确实确实有这种可能,那么我们就可以看下面案例三。案例三:#include<stdio.h> void a(){ char *s1="hello"; printf("%0x\n",s1); s1="world"; } void b(){ char *s2="hello"; printf("%0x\n",s2); s2="world"; } int main(){ char *s3=(char *)0; a(); b(); s3="hello"; printf("%0x\n",s3); return 0; }三个字符串指针变量指向的地址还是一样的,如图:分析:当我们去掉全局指针变量后,其结果依旧是s1,s2,s3 三个指针变量指向的地址依旧是没有变。在运行完a(),b()两个函数之后,我们再将”hello”赋值给空指针s3指针,其指向地址与s1,s2都一样的。因此我们可以得出结论:==一旦有字符串常量在运行期间创建,就会在内存中一直保持到程序结束==,当使用相同的字符串常量的时候,不会再创建字符串常量,而是指向之前的那个。因此字符串常量是贯穿整个程序的生命周期的。附:既然说到这里了,那就再多说一点。案例:当将 char* s0 改为char s0[ ]时。代码:#include<stdio.h> char s0[]="hello"; void a(){ char *s1="hello"; printf("%0x\n",s1); s1="world"; } void b(){ char *s2="hello"; printf("%0x\n",s2); s2="world"; } int main(){ char *s3=(char *)0; printf("%0x\n",s0); a(); b(); s3="hello"; printf("%0x\n",s3); return 0; }运行结果:s0与s1 、s2、 s3指向的地址不同。分析:造成这样的结果的原因是因为char s0[]="hello",而s0指向的是该数组的首地址,而不是字符串的首地址。而s1、 s2、 s3都是指向字符串的首地址,因此不同。#include <stdio.h> int main(int argc, char *argv[]) { printf("Hello C-Free!\n"); //定义一个数组变量,用字符串常量初始化其值 。 char a[] = "123"; //定义一个字符指针,再定义一个字符串常量,指针指向的常量首地址 const char* b = "321"; const char* c = "321"; //打印变量在内存里的地址,栈区【高地址->低地址】 printf("%d,%d,%d\n",&a, &b, &c);//6356772,6356768,6356764 //打印指针的首地址,b和c相同因为都指向同一常量区 printf("%d,%d,%d\n",a, b, c); //6356772,4206607,4206607 //打印内容 printf("%s,%s,%s\n",a, b, c); //123,321,321 //从b内存拷贝3个字节给a变量,正常 memcpy(a, b, 3); //从a内存拷贝3个字节给a常量指针,编译失败或运行失败,常量不可修改 memcpy(b, a, 3); printf("%s,%s,%s\n",a, b, c); //123,321,321 return 0; }5.字符串常量定义定义:用双引号(“”)括起来的0个或者多个字符组成的序列。存储:每个字符串尾自动加一个 ‘\0’ 作为字符串结束标志。字符串常量在内存的常量存储区是按顺序存储的,如:char* a = "123"; char* b = "456"; char* c = "456";定义a时,存储一个“123\0”;定义b时,判断存储区是否有"456\0",发现没有则在后面追加"456\0";定义c时,判断存储区是否有"456\0",发现有,则不再存储,此时b和c两个指针存储的都是"456\0"这片内存地址既然是常量,那么不可被修改,所以memcpy(b, a, 3);是错误的。6.字符数组char a[] = "123";首先声明一个字符数组a,大小没有确定,但是将一个字符串常量“123\0”赋值给了a,故a的length就是4个字节。【注意“123\0”并没有存储在常量区】注意a归根结底是一个数组,数组是一个变量,不是指针,虽然可把a当做一个指针,因为它指向数组的首地址,但归根结底不是指针。正因为a是变量,所以a能够修改其存储的值。比如:int main(){ char a[]="123"; a[0]='a'; printf("%s\n",a); }输出结果:7.字符指针const char* b = "321";既然名字是字符指针,那么它一定是个指针,指针存储地址。故解读这句就是:首先声明一个字符指针b,然后定义一个字符串常量"321\0"。字符串常量存储在常量区,b存储在栈区,b存储的值是字符串常量"321\0"的地址。这里用到了const,在C语言里不加const也行,C++里不加会有个警告,但不影响编译。但是建议加上const,能够让程序员一眼就知道此指针指向的是常量,也就是最终内容无法修改。虽然最终指向不能修改,但是指针自身的取值,是可以修改的,即可修改指针指向的地方。8.内存图所有存储类型代码区、常量区、静态区(全局区)、堆区、栈区,只有栈是从高地址往低地址存储,其他都是低地址往高地址存储。9.补充来分析几段代码。代码:test0.c#include <stdio.h> #include <stdlib.h> char* toStr() { char *s = "hello word!"; return s; } int main(void) { printf("%s\n", toStr()); }运行结果:运行没有任何问题,因为 “hello world” 是一个字符串常量,存放在文字常量区,把该字符串常量存放的地址赋值给了指针 s,toStr 函数退出时,该该字符串常量所在内存不会被回收,故能够通过指针顺利无误的访问。代码:test1.c#include <stdio.h> #include <stdlib.h> char* toStr() { char s[] = "hello word!"; return s; } int main(void) { printf("%s\n", toStr()); }编译运行结果:调用 toStr 函数,定义了一个局部变量( char [] 型数组),该局部变量存放在栈中,当 toStr 函数退出时,栈要清空,局部变量的内存也被清空了,所以这时的函数返回的是一个已被释放的内存地址,出现段错误。如果函数的返回值非要是一个局部变量的地址,那么该局部变量一定要申明为static类型。代码:test2.c#include <stdio.h> char *returnStr() { static char p[]="hello world!"; return p; } int main() { char *str=NULL; str=returnStr(); printf("%s\n", str); return 0; }运行结果:综合性代码:#include <stdio.h> //返回的是局部变量的地址,该地址位于动态数据区,栈里 char *s1() { char p[] = "Hello world!"; printf("in s1 p=%p\n", p); printf("in s1: string's address: %p\n", &("Hello world!")); return p; } //返回的是字符串常量的地址,该地址位于静态数据区 char *s2() { char *q = "Hello world!"; printf("in s2 q=%p\n", q); printf("in s2: string's address: %p\n", &("Hello world!")); return q; } //返回的是静态局部变量的地址,该地址位于静态数据区 char *s3() { static char r[] = "Hello world!"; printf("in s3 r=%p\n", r); printf("in s3: string's address: %p\n", &("Hello world!")); return r; } int main() { char *t1, *t2, *t3; t1 = s1(); t2 = s2(); t3 = s3(); printf("\nin main:\n"); printf("p = %p\nq = %p\nr = %p\n", t1, t2, t3); // printf("%s\n", t1); /* 运行会出现段错误,先屏蔽 */ printf("%s\n", t2); printf("%s\n", t3); return 0; }运行结果:根据运行结果我们就很清楚了,这里不再赘述。:bookmark: 部分参考文章:(37条消息) 关于字符串常量在内存中的生命周期_w_16822的博客-CSDN博客(39条消息) 【C语言入门】笔记十 (指针中)_Liues233的博客-CSDN博客_假设字符数组sa为一个英文字符串,设计程序,当用户用键盘输入字符串sa后,将(40条消息) C语言—内存的管理和释放_^^不加糖^^的博客-CSDN博客_c语言释放内存
文章
存储  ·  程序员  ·  编译器  ·  C语言  ·  C++  ·  容器
2023-02-01
1 2 3 4 5 6 7 8 9
...
20
跳转至:
开发与运维
5622 人关注 | 131425 讨论 | 301869 内容
+ 订阅
  • 记CentOS7.6 zabix5.0-0 —agent2监控Mysql数据库
  • 【算法日记】快速幂:关于我知道答案却做不出来这档事
  • 【Java实用技术】java中关于整数的几个冷知识,总有一个你不知道
查看更多 >
大数据
188297 人关注 | 29195 讨论 | 80458 内容
+ 订阅
  • 数据结构 | 堆的向上调整和向下调整算法【奇妙的堆排序】
  • How to Deploy a Neural Network on TH1520
  • 我开源了一个Go学习仓库|笔记分享(三)
查看更多 >
云原生
233929 人关注 | 11329 讨论 | 45277 内容
+ 订阅
  • 淘宝小游戏背后的质量保障方案
  • 天猫汽车商详页的SSR改造实践
  • Logback基本使用
查看更多 >
数据库
252583 人关注 | 50737 讨论 | 94860 内容
+ 订阅
  • 记CentOS7.6 zabix5.0-0 —agent2监控Mysql数据库
  • Run 32-bit applications on 64-bit Linux kernel
  • Logback基本使用
查看更多 >
安全
1191 人关注 | 23954 讨论 | 81262 内容
+ 订阅
  • 记CentOS7.6 zabix5.0-0 —agent2监控Mysql数据库
  • 玩转 Go 生态|Hertz WebSocket 扩展简析
  • 使用 Go HTTP 框架 Hertz 进行 JWT 认证
查看更多 >