grunt 插件_从Grunt测试Grunt插件

grunt 插件

编写针对grunt插件的测试结果比预期的要简单。 我需要运行多个任务配置,并想通过在主目录中键入grunt test来调用它们。

在第一个任务失败后,咕声通常会退出。 这使得不可能在主项目gruntfile中存储多个失败方案。 从那里运行它们将需要--force选项,但是grunt会忽略所有不是最佳的警告。

较干净的解决方案是在单独的目录中有一堆gruntfile,然后从主项目gruntfile调用它们。 这篇文章解释了如何做到这一点。

示范项目

演示项目是带有一个grunt任务的小型grunt插件。 根据action options属性的值,任务要么失败并显示警告,要么将成功消息打印到控制台中。

任务:

grunt.registerMultiTask('plugin_tester', 'Demo grunt task.', function() {//merge supplied options with default optionsvar options = this.options({ action: 'pass', message: 'unknown error'});//pass or fail - depending on configured optionsif (options.action==='pass') {grunt.log.writeln('Plugin worked correctly passed.');} else {grunt.warn('Plugin failed: ' + options.message);}
});

有三种不同的方式编写grunt插件单元测试。 每个解决方案在test目录中都有自己的nodeunit文件,并在这篇文章中进行说明:

  • plugin_exec_test.js –最实用的解决方案 ,
  • plugin_fork_test.js – 解决了先前解决方案失败的罕见情况,
  • plugin_spawn_test.js – 可能 ,但最不实用。

所有这三个演示测试都包含三种不同的任务配置:

// Success scenario
options: { action: 'pass' }
// Fail with "complete failure" message
options: { action: 'fail', message: 'complete failure' }
//Fail with "partial failure" message
options: { action: 'fail', message: 'partial failure' }

每个配置都存储在test目录内的单独gruntfile中。 例如,存储在gruntfile-pass.js文件中的成功方案如下所示:

grunt.initConfig({// prove that npm plugin works toojshint: { all: [ 'gruntfile-pass.js' ] },// Configuration to be run (and then tested).plugin_tester: { pass: { options: { action: 'pass' } } }
});// Load this plugin's task(s).
grunt.loadTasks('./../tasks');
// next line does not work - grunt requires locally installed plugins
grunt.loadNpmTasks('grunt-contrib-jshint');grunt.registerTask('default', ['plugin_tester', 'jshint']);

这三个测试gruntfiles看起来几乎相同,只有plugin_tester目标的options对象改变了。

从子目录运行Gruntfile

我们的测试gruntfiles存储在test子目录中,而grunt不能很好地处理这种情况。 本章介绍了问题所在,并介绍了两种解决方法。

问题

要查看问题,请转到演示项目目录并运行以下命令:

grunt --gruntfile test/gruntfile-problem.js

Grunt响应以下错误:

Local Npm module "grunt-contrib-jshint" not found. Is it installed?
Warning: Task "jshint" not found. Use --force to continue.Aborted due to warnings.

说明

Grunt假定grunfile和node_modules存储库存储在同一目录中。 虽然node.js require函数会在所有父目录中搜索所需模块,但loadNpmTasks不会。

这个问题有两种可能的解决方案,一种简单而有趣:

  • 在测试目录( 简单 )中创建本地npm存储库,
  • 从父目录中执行繁重的加载任务( fancy )。

尽管第一个“简单”解决方案比较干净,但演示项目使用了第二个“精美”解决方案。

解决方案1:复制Npm存储库

主要思想很简单,只需在tests目录内创建另一个本地npm存储库:

  • package.json文件复制到tests目录。
  • 向其中添加仅测试依赖项。
  • 每次运行测试时,请运行npm install命令。

这是更清洁的解决方案。 它只有两个缺点:

  • 测试依赖项必须单独维护,
  • 所有插件依赖项都必须安装在两个位置。

解决方案2:从父目录加载Grunt任务

另一个解决方案是强制grunt从存储在另一个目录中的npm存储库加载任务。

Grunt插件加载

Grunt有两种方法可以加载插件:

  • loadTasks('directory-name') –将所有任务加载到目录中,
  • loadNpmTasks('plugin-name') –加载插件定义的所有任务。

loadNpmTasks函数采用grunt插件和模块存储库的固定目录结构。 它猜测应该存储任务的目录名称,然后调用loadTasks('directory-name')函数。

本地npm存储库为每个npm软件包都有单独的子目录。 所有grunt插件都应该具有tasks子目录,并且其中的.js文件都包含任务。 例如, loadNpmTasks('grunt-contrib-jshint')调用从node_mudules/grunt-contrib-jshint/tasks目录加载任务,等效于:

grunt.loadTasks('node_modules/grunt-contrib-jshint/tasks')

因此,如果要从父目录加载grunt-contrib-jshint插件的所有任务,可以执行以下操作:

grunt.loadTasks('../node_modules/grunt-contrib-jshint/tasks')

循环父目录

更为灵活的解决方案是遍历所有父目录,直到找到最近的node_modules存储库或到达根目录为止。 这是在grunt-hacks.js模块内部实现的。

loadParentNpmTasks函数循环父目录:

module.exports = new function() {this.loadParentNpmTasks = function(grunt, pluginName) {var oldDirectory='', climb='', directory, content;// search for the right directorydirectory = climb+'node_modules/'+ pluginName;while (continueClimbing(grunt, oldDirectory, directory)) {climb += '../';oldDirectory = directory;directory = climb+'node_modules/'+ pluginName;}// load tasks or return an errorif (grunt.file.exists(directory)) {grunt.loadTasks(directory+'/tasks');} else {grunt.fail.warn('Tasks plugin ' + pluginName + ' was not found.');}}function continueClimbing(grunt, oldDirectory, directory) {return !grunt.file.exists(directory) &&!grunt.file.arePathsEquivalent(oldDirectory, directory);}}();

修改后的Gruntfile

最后,我们需要通过以下步骤替换grunt.loadNpmTasks('grunt-contrib-jshint')中通常的grunt.loadNpmTasks('grunt-contrib-jshint')调用:

var loader = require("./grunt-hacks.js");
loader.loadParentNpmTasks(grunt, 'grunt-contrib-jshint');

缩短的gruntfile:

module.exports = function(grunt) {var loader = require("./grunt-hacks.js");grunt.initConfig({jshint: { /* ... */  },plugin_tester: { /* ... */ }});grunt.loadTasks('./../tasks');loader.loadParentNpmTasks(grunt, 'grunt-contrib-jshint');
};

缺点

该解决方案有两个缺点:

  • 它不处理集合插件。
  • 如果grunt曾经改变grunt插件的预期结构,则必须修改解决方案。

如果您还需要集合插件,请查看grunts task.js以了解如何支持它们。

从Java脚本调用Gruntfile

我们需要做的第二件事是从javascript调用gruntfile。 唯一的麻烦是,咕unt声会在任务失败时退出整个过程。 因此,我们需要从子进程中调用它。

节点模块子进程具有三种不同的功能,能够在子进程内部运行命令:

  • exec –在命令行执行命令,
  • spawn –在命令行上执行命令的方式不同,
  • fork –在子进程中运行节点模块。

第一个是exec ,最易于使用,并在第一章中进行了说明。 第二章介绍了如何使用fork以及为什么它不如exec最佳。 第三章是关于生成。

执行力

Exec在子进程中运行命令行命令。 您可以指定要在哪个目录中运行它,设置环境变量,设置超时,然后在该超时后将命令终止。 当命令完成运行时,exec调用回调并将其传递给stdout流,stderr流和命令崩溃时的错误。

除非另有配置,否则命令将在当前目录中运行。 我们希望它在tests子目录中运行,所以我们必须指定options对象的cwd属性: {cwd: 'tests/'}

stdout和stderr流内容都存储在缓冲区中。 每个缓冲区的最大大小设置为204800,如果命令产生更多输出,则exec调用将崩溃。 这笔钱足以应付我们的小任务。 如果需要更多,则必须设置maxBuffer options属性。

致电执行

以下代码段显示了如何从exec运行gruntfile。 该函数是异步的,并在完成之后调用whenDoneCallback

var cp = require("child_process");function callGruntfile(filename, whenDoneCallback) {var command, options;command = "grunt --gruntfile "+filename+" --no-color";options = {cwd: 'test/'};cp.exec(command, options, whenDoneCallback);
}

注意:如果将npm安装到测试目录( 简单解决方案 ),则需要使用callNpmInstallAndGruntfile函数而不是callGruntfile

function callNpmInstallAndGruntfile(filename, whenDoneCallback) {var command, options;command = "npm install";options = {cwd: 'test/'};cp.exec(command, {}, function(error, stdout, stderr) {callGruntfile(filename, whenDoneCallback);});
}

单元测试

第一节点单元测试运行成功方案,然后检查流程是否成功完成而没有失败,标准输出是否包含预期的消息以及标准错误是否为空。

成功场景单元测试:

pass: function(test) {test.expect(3);callGruntfile('gruntfile-pass.js', function (error, stdout, stderr) {test.equal(error, null, "Command should not fail.");test.equal(stderr, '', "Standard error stream should be empty.");var stdoutOk = contains(stdout, 'Plugin worked correctly.');test.ok(stdoutOk, "Missing stdout message.");test.done();});
},

第二节点单元测试运行“完全失败”方案,然后检查进程是否按预期失败。 请注意,标准错误流为空,警告被打印到标准输出中。

失败的场景单元测试:

fail_1: function(test) {test.expect(3);var gFile = 'gruntfile-fail-complete.js';callGruntfile(gFile, function (error, stdout, stderr) {test.equal(error, null, "Command should have failed.");test.equal(error.message, 'Command failed: ', "Wrong error message.");test.equal(stderr, '', "Non empty stderr.");var stdoutOk = containsWarning(stdout, 'complete failure');test.ok(stdoutOk, "Missing stdout message.");test.done();});
}

第三次“部分故障”节点单元测试与之前的测试几乎相同。 整个测试文件可在github上找到 。

缺点

坏处:

  • 必须预先设置最大缓冲区大小。

叉子

Fork在子进程中运行node.js模块,等效于在命令行上调用node <module-name> 。 Fork使用回调将标准输出和标准错误发送给调用方。 两个回调都可以被多次调用,并且调用方可以分段获取子进程的输出。

仅在需要处理任意大小的stdout和stderr或需要自定义grunt功能时,使用fork才有意义。 如果您不这样做,则exec更易于使用。

本章分为四个子章节:

  • 从javascript 呼叫grunt ,
  • 读取节点模块中的命令行参数,
  • 在子进程中启动节点模块,
  • 编写单元测试。

呼唤咕unt声

Grunt并非以编程方式被调用。 它没有公开“公共” API,也没有对其进行记录。

我们的解决方案模仿了grunt-cli的功能,因此相对安全。 Grunt-cli与grunt核心分开分发,因此更改的可能性较小。 但是,如果确实更改,则此解决方案也必须更改。

从javascript运行咕unt声需要我们执行以下操作:

  • 将gruntfile名称与其路径分开,
  • 更改活动目录,
  • 调用grunts tasks功能。

从javascript呼叫grunt:

this.runGruntfile = function(filename) {var grunt = require('grunt'), path = require('path'), directory, filename;// split filename into directory and filedirectory = path.dirname(filename);filename = path.basename(filename);//change directoryprocess.chdir(directory);//call gruntgrunt.tasks(['default'], {gruntfile:filename, color:false}, function() {console.log('done');});
};

模块参数

该模块将从命令行调用。 节点将命令行参数保留在内部
process.argv数组:

module.exports = new function() {var filename, directory;this.runGruntfile = function(filename) {/* ... */};//get first command line argumentfilename = process.argv[2];this.runGruntfile(filename);
}();

呼叫叉

Fork具有三个参数:模块的路径,带有命令行参数的数组和options对象。 使用tests/Gruntfile-1.js参数调用module.js

child = cp.fork('./module.js', ['tests/Gruntfile-1.js'], {silent: true})

silent: true选项使返回的child进程的stdout和stderr在父级内部可用。 如果将其设置为true,则返回的对象将提供对调用者的stdoutstderr流的访问。

在每个流上调用on('data', callback) 。 每次子进程向流发送某些内容时,都会调用传递的回调:

child.stdout.on('data', function (data) {console.log('stdout: ' + data); // handle piece of stdout
});
child.stderr.on('data', function (data) {console.log('stderr: ' + data); // handle piece of stderr
});

子进程可能崩溃或正常结束其工作:

child.on('error', function(error){// handle child crashconsole.log('error: ' + error); 
});
child.on('exit', function (code, signal) {// this is called after child process endedconsole.log('child process exited with code ' + code); 
});

演示项目使用以下函数来调用fork和绑定回调:

/*** callbacks: onProcessError(error), onProcessExit(code, signal), onStdout(data), onStderr(data)*/
function callGruntfile(filename, callbacks) {var comArg, options, child;callbacks = callbacks || {};child = cp.fork('./test/call-grunt.js', [filename], {silent: true});if (callbacks.onProcessError) {child.on("error", callbacks.onProcessError);}if (callbacks.onProcessExit) {child.on("exit", callbacks.onProcessExit);}if (callbacks.onStdout) {child.stdout.on('data', callbacks.onStdout);}if (callbacks.onStderr) {child.stderr.on('data', callbacks.onStderr);}
}

编写测试

每个单元测试都调用callGruntfile函数。 回调会在标准输出流中搜索所需的内容,检查退出代码是否正确,在错误流中出现错误时失败,或者在fork调用返回错误时失败。

成功场景单元测试:

pass: function(test) {var wasPassMessage = false, callbacks;test.expect(2);callbacks = {onProcessError: function(error) {test.ok(false, "Unexpected error: " + error);test.done();},onProcessExit: function(code, signal) {test.equal(code, 0, "Exit code should have been 0");test.ok(wasPassMessage, "Pass message was never sent ");test.done();},onStdout: function(data) {if (contains(data, 'Plugin worked correctly.')) {wasPassMessage = true;}},onStderr: function(data) {test.ok(false, "Stderr should have been empty: " + data);}};callGruntfile('test/gruntfile-pass.js', callbacks);
}

对应于失败场景的测试几乎相同,可以在github上找到。

缺点

缺点:

  • 使用的grunt函数不属于官方API。
  • 子进程输出流以块而不是一个大块的形式提供。

产生

Spawn是fork和exec之间的交叉。 与exec类似,spawn能够运行可执行文件并向其传递命令行参数。 子进程输出流的处理方式与fork中的处理方式相同。 它们通过回调分段发送给父级。 因此,与使用fork一样,仅当需要任意大小的stdout或stderr时,使用spawn才有意义。

问题

产卵的主要问题发生在Windows上。 必须准确指定要运行的命令的名称。 如果使用参数grunt调用spawn,则spawn期望可执行文件名不带后缀。 grunt.cmd真正的grunt可执行文件grunt.cmd 。 否则, spawn 忽略Windows环境变量PATHEXT 。

循环后缀

如果要从spawn调用grunt ,则需要执行以下操作之一:

  • 针对Windows和Linux使用不同的代码,或者
  • 从环境中读取PATHEXT并循环遍历,直到找到正确的后缀。

以下函数循环遍历PATHEXT并将正确的文件名传递给回调:

function findGruntFilename(callback) {var command = "grunt", options, extensionsStr, extensions, i, child, onErrorFnc, hasRightExtension = false;onErrorFnc = function(data) {if (data.message!=="spawn ENOENT"){grunt.warn("Unexpected error on spawn " +extensions[i]+ " error: " + data);}};function tryExtension(extension) {var child = cp.spawn(command + extension, ['--version']);child.on("error", onErrorFnc);child.on("exit", function(code, signal) {hasRightExtension = true;callback(command + extension);});}extensionsStr = process.env.PATHEXT || '';extensions = [''].concat(extensionsStr.split(';'));for (i=0; !hasRightExtension && i<extensions.length;i++) {tryExtension(extensions[i]);}
}

编写测试

一旦有了grunt命令名,就可以调用spawn 。 Spawn会触发与fork完全相同的事件,因此
callGruntfile接受完全相同的回调对象,并将其属性绑定到子进程事件:

function callGruntfile(command, filename, callbacks) {var comArg, options, child;callbacks = callbacks || {};comArg = ["--gruntfile", filename, "--no-color"];options = {cwd: 'test/'};child = cp.spawn(command, comArg, options);if (callbacks.onProcessError) {child.on("error", callbacks.onProcessError);}/* ... callbacks binding exactly as in fork ...*/
}

测试也几乎与上一章中的测试相同。 唯一的区别是,在执行其他所有操作之前,您必须先找到grunt可执行文件名。 成功场景测试如下所示:

pass: function(test) {var wasPassMessage = false;test.expect(2);findGruntFilename(function(gruntCommand){var callbacks = {/* ... callbacks look exactly the same way as in fork ... */};callGruntfile(gruntCommand, 'gruntfile-pass.js', callbacks);});
}

完整的成功方案测试以及两个失败方案测试都可以在github上获得 。

缺点

缺点:

  • Spawn会忽略PATHEXT后缀,需要使用自定义代码来处理它。
  • 子进程输出流以块而不是一个大块的形式提供。

结论

有三种方法可以从gruntfile内部测试grunt插件。 除非您有非常强烈的理由不这样做,否则请使用exec

翻译自: https://www.javacodegeeks.com/2015/02/testing-grunt-plugin-from-grunt.html

grunt 插件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/338407.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

​嵌入式开发为什么选择C语言?

从语言特点来说C语言有出色的可移植性&#xff0c;能在多种不同体系结构的软/硬平台上运行。简洁紧凑&#xff0c;使用灵活的语法机制&#xff0c;并能直接访问硬件能够直接访问硬件的语言有&#xff1a;汇编和C语言汇编属于低级语言&#xff0c;难以完成一些复杂的功能&#x…

mysql 临时表 heap_mysql优化: 内存表和临时表

由于直接使用临时表来创建中间表&#xff0c;其速度不如人意&#xff0c;因而就有了把临时表建成内存表的想法。但内存表和临时表的区别且并不熟悉&#xff0c;需要查找资料了。一开始以为临时表是创建后存在&#xff0c;当连接断开时临时表就会被删除&#xff0c;即临时表是存…

序列化与反序列化的单例模式_序列化代理模式

序列化与反序列化的单例模式在上一篇文章中 &#xff0c;我谈到了一般的序列化。 这是更加集中的内容&#xff0c;并提供了一个细节&#xff1a; 序列化代理模式 。 这是处理序列化中许多问题的一种好方法&#xff0c;通常是最好的方法。 如果开发人员只想了解这一主题&#xf…

图解C语言的希尔排序

希尔排序是插入排序的一种&#xff0c;又称“缩小增量排序”&#xff0c;希尔排序是直接插入排序算法的一种更高效的改进版本。希尔排序的基本思想设等待排序等元素序列有n个元素&#xff0c;首先取一个整数increment&#xff08;小于n&#xff09;作为间隔将全部元素分为n/inc…

给oim_对OIM Web(UI)层进行压力测试

给oimOracle IDM中的默认配置保留20个专用于服务前端&#xff08;UI&#xff09;请求的线程 。 这基本上意味着应用程序服务器具有20个线程池&#xff0c;可用于为通过Web控制台&#xff08;/ identity或/ sysadmin&#xff09;访问OIM的用户提供服务。 对于Weblogic &#xf…

windows mysql is read only_mysql中Table is read only错误解决方法(转载)

下面来给各位同学介绍一下关于mysql中Table is read only的解决技巧&#xff0c;希望例子能帮助到各位。今天再我把数据库data 拷贝到linux 下运行程序 ”mysql中Table is read only的解决“ 出现这样的问题&#xff0c;查询资料。linux下执行如下命令即可#mysqladmin -u root…

C语言打印输出红色字体

除了Linux&#xff0c;在VS下也可以实现变色这一效果&#xff0c;先看下面的一段代码&#xff1a;#include int main(int argc,char **argv){ printf("\033[44;37;5m hello world\033[0m\n");return 0;}编译后运行上述代码&#xff0c;结果如下&#xff1a;可见&…

mysql g月份分组_PowerBI快捷键——视觉对象分组功能

PowerBI的2020年4月份更新虽然发布在5月份&#xff0c;但的确是提供了很多强大的功能。在以往&#xff0c;要选中多个视觉对象&#xff0c;往往需要按住CTRL键挨个单击选中&#xff0c;然后在进行下一步的分组或其他操作。但是在4月份更新中&#xff0c;PowerBI允许我们通过在画…

编译原理抽象语法树_平衡抽象原理

编译原理抽象语法树使代码复杂易读和理解的一件事是&#xff0c;方法内部的指令处于不同的抽象级别。 假设我们的应用程序仅允许登录用户查看其朋友的旅行。 如果用户不是朋友&#xff0c;则不会显示任何行程。 一个例子&#xff1a; public List<Trip> tripsByFriend…

谈谈单片机编程思想——状态机

玩单片机还可以&#xff0c;各个外设也都会驱动&#xff0c;但是如果让你完整的写一套代码时&#xff0c;却无逻辑与框架可言。这说明编程还处于比较低的水平&#xff0c;你需要学会一种好的编程框架或者一种编程思想&#xff01;比如模块化编程、状态机编程、分层思想等。本文…

C语言结构体使用与指针的理解

以前总有一种疑惑。为什么结构体的指针有的需要用分配空间&#xff1f;有的不需要分配空间呢&#xff1f;现在总结一下思路&#xff1a;一&#xff1a;关于结构体的定义问题&#xff1a;使用结构体一般会使用变量或者定义指针typedef struct{ int a; int b; }data;使用这个结构…

elementui 进度条怎么做_小E,Excel中这样的进度条是怎么做出来的?

我的目标&#xff1a;让中国的大学生走出校门的那一刻就已经具备这些office技能&#xff0c;让职场人士能高效使用office为其服务。支持我&#xff0c;也为自己加油&#xff01;前面我们分享过如何做进度条&#xff1a;《Excel进度条启示&#xff1a;专注与持续积累定会让人生出…

adf4351使用_使用ADF BC管理保存点

adf4351使用在使用ADF BC时&#xff0c;我们通常依赖于在数据库中执行DML操作的框架。 该框架在DBTransaction提交周期内正确地在数据库中进行了所有必要的更新。 很酷的事情是&#xff0c;在这种情况下&#xff0c;数据库事务将被自动管理。 因此&#xff0c;如果出现问题&…

C语言结构体描述BMP的文件格式

BMP文件的结构其实非常简单&#xff0c;就是两个结构体&#xff0b;一个可选的调色板&#xff0b;位图数据。第一个结构体是BITMAPFILEHEADER&#xff0c;第二个结构体是BITMAPINFOHEADER。然后就是可选的调色板&#xff08;RGBQUAD数组&#xff09;。最后是位图数据。第一个结…

java必读书籍_必读:Java Java

java必读书籍他们没有在Comp Sci&#xff0c;工程学或MIS中教appsec&#xff0c;但是您却学会了编程。 而且他们可能仍然没有。 因此&#xff0c;您将如何得知XSS过滤器逃避或单击劫持攻击&#xff0c;或如何真正安全地存储密码。 您的公司无力为您提供昂贵的Appsec培训&#…

php mysql 常用语句_PHP mysql基本语句指令

1 /* 选择数据库 2 use test; 3 */ 4 5 /* 显示所有的数据库 6 show databases; 7 */ 8 9 /* 删除表/数据库 10 drop database test1; 11 delete from user1 where id4; 12 */ 13 14 /* 创建表 15 CREATE TABLE user1( 16 id int primary key auto_increment1 /*选择数据库2 us…

列表流和feed流_通过流而不是列表

列表流和feed流开幕式免责声明&#xff1a;这并不总是一个好主意。 我将介绍这个想法&#xff0c;以及为什么它是一个好主意的一些原因&#xff0c;但随后我将讨论一些不太理想的实例。 懒惰 如您所知&#xff0c;我在Python中的学习几乎和在Java中一样。 我一发现Python就很喜…

mysql数据导出不完正_【MySQL】mysqldump 导出数据 常见问题

1、使用mysqldump时报错(1064)&#xff0c;这个是因为mysqldump版本太低与当前数据库版本不一致导致的mysqldump: Couldnt execute SET OPTION SQL_QUOTE_SHOW_CREATE1: You have an error in your SQL syntax; check the manual thatcorresponds to your MySQL server version…

C语言Main函数到底有几种,你真的懂吗?

乍一看标题&#xff0c;感觉小编小题大做&#xff0c;但凡学过C语言的聚聚&#xff0c;都知道C程序入口就是main函数&#xff0c;且一套程序里面有且仅有一个。但是很多时候我们看到的main函数却并不是千篇一律&#xff0c;格式竟然会有差别&#xff0c;这究竟是为啥&#xff1…

java泛型类指定多个泛型_Java泛型中的多态

java泛型类指定多个泛型从作为Java程序员的早期开始&#xff0c;我们都知道如何实例化和使用Collection对象。 实例化为具体类的List接口将如下所示。 List myArrayList new ArrayList();如果myArrayList应该仅保存Integer对象&#xff0c;则从Java 5编译器开始&#xff0c…